From 27bc7dcb43ec20d185b92986cba06cbc0329b58f Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Thu, 31 May 2018 15:50:02 -0700 Subject: [PATCH] feat(ivy): ngtsc compiles @Component, @Directive, @NgModule (#24427) This change supports compilation of components, directives, and modules within ngtsc. Support is not complete, but is enough to compile and test //packages/core/test/bundling/todo in full AOT mode. Code size benefits are not yet achieved as //packages/core itself does not get compiled, and some decorators (e.g. @Input) are not stripped, leading to unwanted code being retained by the tree-shaker. This will be improved in future commits. PR Close #24427 --- .../src/largetable/render3/table.ts | 4 +- modules/benchmarks/src/tree/render3/tree.ts | 6 +- packages/compiler-cli/BUILD.bazel | 1 + .../src/ngtsc/annotations/BUILD.bazel | 17 + .../src/ngtsc/annotations/index.ts | 13 + .../src/ngtsc/annotations/src/component.ts | 117 ++++++ .../src/ngtsc/annotations/src/directive.ts | 212 +++++++++++ .../src/injectable.ts | 41 +- .../src/ngtsc/annotations/src/ng_module.ts | 116 ++++++ .../ngtsc/annotations/src/selector_scope.ts | 353 ++++++++++++++++++ .../src/ngtsc/annotations/src/util.ts | 84 +++++ .../src/ngtsc/annotations/test/BUILD.bazel | 28 ++ .../annotations/test/selector_scope_spec.ts | 79 ++++ .../src/ngtsc/metadata/BUILD.bazel | 1 + .../compiler-cli/src/ngtsc/metadata/index.ts | 5 +- .../src/ngtsc/metadata/src/reflector.ts | 100 +++++ .../src/ngtsc/metadata/src/resolver.ts | 338 +++++++++++------ .../src/ngtsc/metadata/test/BUILD.bazel | 1 + .../src/ngtsc/metadata/test/resolver_spec.ts | 60 ++- packages/compiler-cli/src/ngtsc/program.ts | 13 +- .../src/ngtsc/testing/in_memory_typescript.ts | 5 +- .../src/ngtsc/transform/BUILD.bazel | 1 + .../compiler-cli/src/ngtsc/transform/index.ts | 2 +- .../src/ngtsc/transform/src/api.ts | 13 +- .../src/ngtsc/transform/src/compilation.ts | 13 +- .../src/ngtsc/transform/src/declaration.ts | 8 +- .../src/ngtsc/transform/src/transform.ts | 78 ++-- .../src/ngtsc/transform/src/translator.ts | 89 ++++- .../src/ngtsc/util/src/visitor.ts | 26 +- .../test/ngtsc/fake_core/index.ts | 2 + .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 60 ++- .../compiler/src/render3/r3_identifiers.ts | 5 + .../src/render3/r3_module_compiler.ts | 7 +- .../compiler/src/render3/view/compiler.ts | 18 +- .../compiler/src/render3/view/template.ts | 4 +- .../core/src/core_render3_private_export.ts | 5 +- packages/core/src/metadata.ts | 2 +- packages/core/src/metadata/directives.ts | 8 + packages/core/src/metadata/ng_module.ts | 50 ++- packages/core/src/render3/component.ts | 9 +- packages/core/src/render3/component_ref.ts | 4 +- packages/core/src/render3/definition.ts | 25 +- packages/core/src/render3/di.ts | 10 +- packages/core/src/render3/hooks.ts | 10 +- packages/core/src/render3/index.ts | 10 +- packages/core/src/render3/instructions.ts | 34 +- .../core/src/render3/interfaces/definition.ts | 28 +- packages/core/src/render3/interfaces/view.ts | 4 +- packages/core/src/render3/jit/directive.ts | 4 +- packages/core/src/render3/jit/environment.ts | 3 +- packages/core/src/render3/jit/module.ts | 18 +- packages/core/src/render3/query.ts | 4 +- .../test/bundling/hello_world/BUILD.bazel | 1 + packages/core/test/bundling/todo/BUILD.bazel | 6 +- .../bundling/todo/bundle.golden_symbols.json | 174 --------- packages/core/test/bundling/todo/index.ts | 31 +- .../component_directives_spec.ts | 51 +-- .../compiler_canonical/elements_spec.ts | 5 +- .../compiler_canonical/life_cycle_spec.ts | 5 +- .../render3/compiler_canonical/pipes_spec.ts | 8 +- .../render3/compiler_canonical/query_spec.ts | 6 +- .../compiler_canonical/small_app_spec.ts | 2 +- .../template_variables_spec.ts | 4 +- packages/core/test/render3/component_spec.ts | 4 +- packages/core/test/render3/define_spec.ts | 22 +- packages/core/test/render3/ivy/jit_spec.ts | 12 +- .../core/test/render3/jit_environment_spec.ts | 1 + packages/core/test/render3/render_util.ts | 6 +- packages/core/testing/src/test_bed.ts | 5 +- 69 files changed, 1884 insertions(+), 607 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/annotations/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/component.ts create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/directive.ts rename packages/compiler-cli/src/ngtsc/{transform => annotations}/src/injectable.ts (78%) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/util.ts create mode 100644 packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts diff --git a/modules/benchmarks/src/largetable/render3/table.ts b/modules/benchmarks/src/largetable/render3/table.ts index 85b43102ce..881f487525 100644 --- a/modules/benchmarks/src/largetable/render3/table.ts +++ b/modules/benchmarks/src/largetable/render3/table.ts @@ -7,7 +7,7 @@ */ import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core'; -import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; +import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition'; import {TableCell, buildTable, emptyTable} from '../util'; @@ -15,7 +15,7 @@ export class LargeTableComponent { data: TableCell[][] = emptyTable; /** @nocollapse */ - static ngComponentDef: ComponentDef = defineComponent({ + static ngComponentDef: ComponentDefInternal = defineComponent({ type: LargeTableComponent, selectors: [['largetable']], template: function(rf: RenderFlags, ctx: LargeTableComponent) { diff --git a/modules/benchmarks/src/tree/render3/tree.ts b/modules/benchmarks/src/tree/render3/tree.ts index 61bd0955e0..8b629df4b5 100644 --- a/modules/benchmarks/src/tree/render3/tree.ts +++ b/modules/benchmarks/src/tree/render3/tree.ts @@ -7,7 +7,7 @@ */ import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core'; -import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; +import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition'; import {TreeNode, buildTree, emptyTree} from '../util'; @@ -35,7 +35,7 @@ export class TreeComponent { data: TreeNode = emptyTree; /** @nocollapse */ - static ngComponentDef: ComponentDef = defineComponent({ + static ngComponentDef: ComponentDefInternal = defineComponent({ type: TreeComponent, selectors: [['tree']], template: function(rf: RenderFlags, ctx: TreeComponent) { @@ -95,7 +95,7 @@ export class TreeFunction { data: TreeNode = emptyTree; /** @nocollapse */ - static ngComponentDef: ComponentDef = defineComponent({ + static ngComponentDef: ComponentDefInternal = defineComponent({ type: TreeFunction, selectors: [['tree']], template: function(rf: RenderFlags, ctx: TreeFunction) { diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 867afd5333..fcdf1eb025 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -25,6 +25,7 @@ ts_library( tsconfig = ":tsconfig", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/transform", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel new file mode 100644 index 0000000000..0e502d94c4 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -0,0 +1,17 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "annotations", + srcs = glob([ + "index.ts", + "src/**/*.ts", + ]), + module_name = "@angular/compiler-cli/src/ngtsc/annotations", + deps = [ + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/transform", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/annotations/index.ts b/packages/compiler-cli/src/ngtsc/annotations/index.ts new file mode 100644 index 0000000000..f56e3074cc --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {ComponentDecoratorHandler} from './src/component'; +export {DirectiveDecoratorHandler} from './src/directive'; +export {InjectableDecoratorHandler} from './src/injectable'; +export {NgModuleDecoratorHandler} from './src/ng_module'; +export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts new file mode 100644 index 0000000000..13894bdf82 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {Decorator, reflectNonStaticField, reflectObjectLiteral, staticallyResolve} from '../../metadata'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; + +import {extractDirectiveMetadata} from './directive'; +import {SelectorScopeRegistry} from './selector_scope'; + +const EMPTY_MAP = new Map(); + +/** + * `DecoratorHandler` which handles the `@Component` annotation. + */ +export class ComponentDecoratorHandler implements DecoratorHandler { + constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} + + detect(decorators: Decorator[]): Decorator|undefined { + return decorators.find( + decorator => decorator.name === 'Component' && decorator.from === '@angular/core'); + } + + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + const meta = decorator.args[0]; + if (!ts.isObjectLiteralExpression(meta)) { + throw new Error(`Decorator argument must be literal.`); + } + + // @Component inherits @Directive, so begin by extracting the @Directive metadata and building + // on it. + const directiveMetadata = extractDirectiveMetadata(node, decorator, this.checker); + if (directiveMetadata === undefined) { + // `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this + // case, compilation of the decorator is skipped. Returning an empty object signifies + // that no analysis was produced. + return {}; + } + + // Next, read the `@Component`-specific fields. + const component = reflectObjectLiteral(meta); + + // Resolve and parse the template. + if (!component.has('template')) { + throw new Error(`For now, components must directly have a template.`); + } + const templateExpr = component.get('template') !; + const templateStr = staticallyResolve(templateExpr, this.checker); + if (typeof templateStr !== 'string') { + throw new Error(`Template must statically resolve to a string: ${node.name!.text}`); + } + + let preserveWhitespaces: boolean = false; + if (component.has('preserveWhitespaces')) { + const value = staticallyResolve(component.get('preserveWhitespaces') !, this.checker); + if (typeof value !== 'boolean') { + throw new Error(`preserveWhitespaces must resolve to a boolean if present`); + } + preserveWhitespaces = value; + } + + const template = parseTemplate( + templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`, + {preserveWhitespaces}); + if (template.errors !== undefined) { + throw new Error( + `Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`); + } + + // If the component has a selector, it should be registered with the `SelectorScopeRegistry` so + // when this component appears in an `@NgModule` scope, its selector can be determined. + if (directiveMetadata.selector !== null) { + this.scopeRegistry.registerSelector(node, directiveMetadata.selector); + } + + return { + analysis: { + ...directiveMetadata, + template, + viewQueries: [], + + // These will be replaced during the compilation step, after all `NgModule`s have been + // analyzed and the full compilation scope for the component can be realized. + pipes: EMPTY_MAP, + directives: EMPTY_MAP, + } + }; + } + compile(node: ts.ClassDeclaration, analysis: R3ComponentMetadata): CompileResult { + const pool = new ConstantPool(); + + // Check whether this component was registered with an NgModule. If so, it should be compiled + // under that module's compilation scope. + const scope = this.scopeRegistry.lookupCompilationScope(node); + if (scope !== null) { + // Replace the empty components and directives from the analyze() step with a fully expanded + // scope. This is possible now because during compile() the whole compilation unit has been + // fully analyzed. + analysis = {...analysis, ...scope}; + } + + const res = compileComponentFromMetadata(analysis, pool, makeBindingParser()); + return { + field: 'ngComponentDef', + initializer: res.expression, + statements: pool.statements, + type: res.type, + }; + } +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts new file mode 100644 index 0000000000..f9e49f69f2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -0,0 +1,212 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {Decorator, staticallyResolve} from '../../metadata'; +import {DecoratedNode, getDecoratedClassElements, reflectNonStaticField, reflectObjectLiteral} from '../../metadata/src/reflector'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; + +import {SelectorScopeRegistry} from './selector_scope'; +import {getConstructorDependencies} from './util'; + +const EMPTY_OBJECT: {[key: string]: string} = {}; + +export class DirectiveDecoratorHandler implements DecoratorHandler { + constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} + + detect(decorators: Decorator[]): Decorator|undefined { + return decorators.find( + decorator => decorator.name === 'Directive' && decorator.from === '@angular/core'); + } + + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + const analysis = extractDirectiveMetadata(node, decorator, this.checker); + + // If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so + // when this directive appears in an `@NgModule` scope, its selector can be determined. + if (analysis && analysis.selector !== null) { + this.scopeRegistry.registerSelector(node, analysis.selector); + } + + return {analysis}; + } + + compile(node: ts.ClassDeclaration, analysis: R3DirectiveMetadata): CompileResult { + const pool = new ConstantPool(); + const res = compileDirectiveFromMetadata(analysis, pool, makeBindingParser()); + return { + field: 'ngDirectiveDef', + initializer: res.expression, + statements: pool.statements, + type: res.type, + }; + } +} + +/** + * Helper function to extract metadata from a `Directive` or `Component`. + */ +export function extractDirectiveMetadata( + clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker): R3DirectiveMetadata| + undefined { + const meta = decorator.args[0]; + if (!ts.isObjectLiteralExpression(meta)) { + throw new Error(`Decorator argument must be literal.`); + } + const directive = reflectObjectLiteral(meta); + + if (directive.has('jit')) { + // The only allowed value is true, so there's no need to expand further. + return undefined; + } + + // Precompute a list of ts.ClassElements that have decorators. This includes things like @Input, + // @Output, @HostBinding, etc. + const decoratedElements = getDecoratedClassElements(clazz, checker); + + // Construct the map of inputs both from the @Directive/@Component decorator, and the decorated + // fields. + const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker); + const inputsFromFields = parseDecoratedFields( + findDecoratedFields(decoratedElements, '@angular/core', 'Input'), checker); + + // And outputs. + const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker); + const outputsFromFields = parseDecoratedFields( + findDecoratedFields(decoratedElements, '@angular/core', 'Output'), checker); + + // Parse the selector. + let selector = ''; + if (directive.has('selector')) { + const resolved = staticallyResolve(directive.get('selector') !, checker); + if (typeof resolved !== 'string') { + throw new Error(`Selector must be a string`); + } + selector = resolved; + } + + // Determine if `ngOnChanges` is a lifecycle hook defined on the component. + const usesOnChanges = reflectNonStaticField(clazz, 'ngOnChanges') !== null; + + return { + name: clazz.name !.text, + deps: getConstructorDependencies(clazz, checker), + host: { + attributes: {}, + listeners: {}, + properties: {}, + }, + lifecycle: { + usesOnChanges, + }, + inputs: {...inputsFromMeta, ...inputsFromFields}, + outputs: {...outputsFromMeta, ...outputsFromFields}, + queries: [], selector, + type: new WrappedNodeExpr(clazz.name !), + typeSourceSpan: null !, + }; +} + +function assertIsStringArray(value: any[]): value is string[] { + for (let i = 0; i < value.length; i++) { + if (typeof value[i] !== 'string') { + throw new Error(`Failed to resolve @Directive.inputs[${i}] to a string`); + } + } + return true; +} + +type DecoratedProperty = DecoratedNode; + +/** + * Find all fields in the array of `DecoratedNode`s that have a decorator of the given type. + */ +function findDecoratedFields( + elements: DecoratedNode[], decoratorModule: string, + decoratorName: string): DecoratedProperty[] { + return elements + .map(entry => { + const element = entry.element; + // Only consider properties and accessors. Filter out everything else. + if (!ts.isPropertyDeclaration(element) && !ts.isAccessor(element)) { + return null; + } + + // Extract the array of matching decorators (there could be more than one). + const decorators = entry.decorators.filter( + decorator => decorator.name === decoratorName && decorator.from === decoratorModule); + if (decorators.length === 0) { + // No matching decorators, don't include this element. + return null; + } + return {element, decorators}; + }) + // Filter out nulls. + .filter(entry => entry !== null) as DecoratedProperty[]; +} + +/** + * Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the + * correctly shaped metadata object. + */ +function parseFieldToPropertyMapping( + directive: Map, field: string, + checker: ts.TypeChecker): {[field: string]: string} { + if (!directive.has(field)) { + return EMPTY_OBJECT; + } + + // Resolve the field of interest from the directive metadata to a string[]. + const metaValues = staticallyResolve(directive.get(field) !, checker); + if (!Array.isArray(metaValues) || !assertIsStringArray(metaValues)) { + throw new Error(`Failed to resolve @Directive.${field}`); + } + + return metaValues.reduce( + (results, value) => { + // Either the value is 'field' or 'field: property'. In the first case, `property` will + // be undefined, in which case the field name should also be used as the property name. + const [field, property] = value.split(':', 2).map(str => str.trim()); + results[field] = property || field; + return results; + }, + {} as{[field: string]: string}); +} + +/** + * Parse property decorators (e.g. `Input` or `Output`) and return the correctly shaped metadata + * object. + */ +function parseDecoratedFields( + fields: DecoratedProperty[], checker: ts.TypeChecker): {[field: string]: string} { + return fields.reduce( + (results, field) => { + const fieldName = (field.element.name as ts.Identifier).text; + field.decorators.forEach(decorator => { + // The decorator either doesn't have an argument (@Input()) in which case the property + // name is used, or it has one argument (@Output('named')). + if (decorator.args.length === 0) { + results[fieldName] = fieldName; + } else if (decorator.args.length === 1) { + const property = staticallyResolve(decorator.args[0], checker); + if (typeof property !== 'string') { + throw new Error(`Decorator argument must resolve to a string`); + } + results[fieldName] = property; + } else { + // Too many arguments. + throw new Error( + `Decorator must have 0 or 1 arguments, got ${decorator.args.length} argument(s)`); + } + }); + return results; + }, + {} as{[field: string]: string}); +} diff --git a/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts similarity index 78% rename from packages/compiler-cli/src/ngtsc/transform/src/injectable.ts rename to packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index d6444bc584..f0210e78c1 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -11,14 +11,15 @@ import * as ts from 'typescript'; import {Decorator} from '../../metadata'; import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform/src/api'; -import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api'; +import {getConstructorDependencies} from './util'; /** * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ -export class InjectableCompilerAdapter implements CompilerAdapter { +export class InjectableDecoratorHandler implements DecoratorHandler { constructor(private checker: ts.TypeChecker) {} detect(decorator: Decorator[]): Decorator|undefined { @@ -31,11 +32,12 @@ export class InjectableCompilerAdapter implements CompilerAdapter { - let tokenExpr = param.typeValueExpr; - let optional = false, self = false, skipSelf = false; - param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => { - if (dec.name === 'Inject') { - if (dec.args.length !== 1) { - throw new Error(`Unexpected number of arguments to @Inject().`); - } - tokenExpr = dec.args[0]; - } else if (dec.name === 'Optional') { - optional = true; - } else if (dec.name === 'SkipSelf') { - skipSelf = true; - } else if (dec.name === 'Self') { - self = true; - } else { - throw new Error(`Unexpected decorator ${dec.name} on parameter.`); - } - if (tokenExpr === null) { - throw new Error(`No suitable token for parameter!`); - } - }); - const token = new WrappedNodeExpr(tokenExpr); - useType.push( - {token, optional, self, skipSelf, host: false, resolved: R3ResolvedDependencyType.Token}); - }); - return useType; -} + function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata { const meta: R3DependencyMetadata = { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts new file mode 100644 index 0000000000..b39e2c561d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ConstantPool, Expression, R3DirectiveMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {Decorator, Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; + +import {SelectorScopeRegistry} from './selector_scope'; +import {referenceToExpression} from './util'; + +/** + * Compiles @NgModule annotations to ngModuleDef fields. + * + * TODO(alxhub): handle injector side of things as well. + */ +export class NgModuleDecoratorHandler implements DecoratorHandler { + constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} + + detect(decorators: Decorator[]): Decorator|undefined { + return decorators.find( + decorator => decorator.name === 'NgModule' && decorator.from === '@angular/core'); + } + + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + const meta = decorator.args[0]; + if (!ts.isObjectLiteralExpression(meta)) { + throw new Error(`Decorator argument must be literal.`); + } + const ngModule = reflectObjectLiteral(meta); + + if (ngModule.has('jit')) { + // The only allowed value is true, so there's no need to expand further. + return {}; + } + + // Extract the module declarations, imports, and exports. + let declarations: Reference[] = []; + if (ngModule.has('declarations')) { + const declarationMeta = staticallyResolve(ngModule.get('declarations') !, this.checker); + declarations = resolveTypeList(declarationMeta, 'declarations'); + } + let imports: Reference[] = []; + if (ngModule.has('imports')) { + const importsMeta = staticallyResolve(ngModule.get('imports') !, this.checker); + imports = resolveTypeList(importsMeta, 'imports'); + } + let exports: Reference[] = []; + if (ngModule.has('exports')) { + const exportsMeta = staticallyResolve(ngModule.get('exports') !, this.checker); + exports = resolveTypeList(exportsMeta, 'exports'); + } + + // Register this module's information with the SelectorScopeRegistry. This ensures that during + // the compile() phase, the module's metadata is available for selector scope computation. + this.scopeRegistry.registerModule(node, {declarations, imports, exports}); + + const context = node.getSourceFile(); + + return { + analysis: { + type: new WrappedNodeExpr(node.name !), + bootstrap: [], + declarations: declarations.map(decl => referenceToExpression(decl, context)), + exports: exports.map(exp => referenceToExpression(exp, context)), + imports: imports.map(imp => referenceToExpression(imp, context)), + emitInline: false, + }, + }; + } + + compile(node: ts.ClassDeclaration, analysis: R3NgModuleMetadata): CompileResult { + const res = compileNgModule(analysis); + return { + field: 'ngModuleDef', + initializer: res.expression, + statements: [], + type: res.type, + }; + } +} + +/** + * Compute a list of `Reference`s from a resolved metadata value. + */ +function resolveTypeList(resolvedList: ResolvedValue, name: string): Reference[] { + const refList: Reference[] = []; + if (!Array.isArray(resolvedList)) { + throw new Error(`Expected array when reading property ${name}`); + } + + resolvedList.forEach((entry, idx) => { + if (Array.isArray(entry)) { + // Recurse into nested arrays. + refList.push(...resolveTypeList(entry, name)); + } else if (entry instanceof Reference) { + if (!entry.expressable) { + throw new Error(`Value at position ${idx} in ${name} array is not expressable`); + } else if (!ts.isClassDeclaration(entry.node)) { + throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`); + } + refList.push(entry); + } else { + // TODO(alxhub): expand ModuleWithProviders. + throw new Error(`Value at position ${idx} in ${name} array is not a reference`); + } + }); + + return refList; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts new file mode 100644 index 0000000000..4ddc245ad7 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts @@ -0,0 +1,353 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Expression, ExternalExpr, ExternalReference} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {AbsoluteReference, Reference, reflectStaticField, reflectTypeEntityToDeclaration} from '../../metadata'; + +import {referenceToExpression} from './util'; + + +/** + * Metadata extracted for a given NgModule that can be used to compute selector scopes. + */ +export interface ModuleData { + declarations: Reference[]; + imports: Reference[]; + exports: Reference[]; +} + +/** + * Transitively expanded maps of directives and pipes visible to a component being compiled in the + * context of some module. + */ +export interface CompilationScope { + directives: Map; + pipes: Map; +} + +/** + * Both transitively expanded scopes for a given NgModule. + */ +interface SelectorScopes { + /** + * Set of components, directives, and pipes visible to all components being compiled in the + * context of some module. + */ + compilation: Reference[]; + + /** + * Set of components, directives, and pipes added to the compilation scope of any module importing + * some module. + */ + exported: Reference[]; +} + +/** + * Registry which records and correlates static analysis information of Angular types. + * + * Once a compilation unit's information is fed into the SelectorScopeRegistry, it can be asked to + * produce transitive `CompilationScope`s for components. + */ +export class SelectorScopeRegistry { + /** + * Map of modules declared in the current compilation unit to their (local) metadata. + */ + private _moduleToData = new Map(); + + /** + * Map of modules to their cached `CompilationScope`s. + */ + private _compilationScopeCache = new Map>(); + + /** + * Map of components/directives to their selector. + */ + private _directiveToSelector = new Map(); + + /** + * Map of pipes to their name. + */ + private _pipeToName = new Map(); + + /** + * Map of components/directives/pipes to their module. + */ + private _declararedTypeToModule = new Map(); + + constructor(private checker: ts.TypeChecker) {} + + /** + * Register a module's metadata with the registry. + */ + registerModule(node: ts.ClassDeclaration, data: ModuleData): void { + node = ts.getOriginalNode(node) as ts.ClassDeclaration; + + if (this._moduleToData.has(node)) { + throw new Error(`Module already registered: ${node.name!.text}`); + } + this._moduleToData.set(node, data); + + // Register all of the module's declarations in the context map as belonging to this module. + data.declarations.forEach(decl => { + this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.ClassDeclaration, node); + }); + } + + /** + * Register the selector of a component or directive with the registry. + */ + registerSelector(node: ts.ClassDeclaration, selector: string): void { + node = ts.getOriginalNode(node) as ts.ClassDeclaration; + + if (this._directiveToSelector.has(node)) { + throw new Error(`Selector already registered: ${node.name!.text} ${selector}`); + } + this._directiveToSelector.set(node, selector); + } + + /** + * Register the name of a pipe with the registry. + */ + registerPipe(node: ts.ClassDeclaration, name: string): void { this._pipeToName.set(node, name); } + + /** + * Produce the compilation scope of a component, which is determined by the module that declares + * it. + */ + lookupCompilationScope(node: ts.ClassDeclaration): CompilationScope|null { + node = ts.getOriginalNode(node) as ts.ClassDeclaration; + + // If the component has no associated module, then it has no compilation scope. + if (!this._declararedTypeToModule.has(node)) { + return null; + } + + const module = this._declararedTypeToModule.get(node) !; + + // Compilation scope computation is somewhat expensive, so it's cached. Check the cache for + // the module. + if (this._compilationScopeCache.has(module)) { + // The compilation scope was cached. + const scope = this._compilationScopeCache.get(module) !; + + // The scope as cached is in terms of References, not Expressions. Converting between them + // requires knowledge of the context file (in this case, the component node's source file). + return convertScopeToExpressions(scope, node.getSourceFile()); + } + + // This is the first time the scope for this module is being computed. + const directives = new Map(); + const pipes = new Map(); + + // Process the declaration scope of the module, and lookup the selector of every declared type. + // The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule + // was not imported from a .d.ts source. + this.lookupScopes(module !, /* ngModuleImportedFrom */ null).compilation.forEach(ref => { + const selector = + this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.ClassDeclaration); + // Only directives/components with selectors get added to the scope. + if (selector != null) { + directives.set(selector, ref); + } + }); + + const scope: CompilationScope = {directives, pipes}; + + // Many components may be compiled in the same scope, so cache it. + this._compilationScopeCache.set(node, scope); + + // Convert References to Expressions in the context of the component's source file. + return convertScopeToExpressions(scope, node.getSourceFile()); + } + + /** + * Lookup `SelectorScopes` for a given module. + * + * This function assumes that if the given module was imported from an absolute path + * (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well + * as imports and exports from other modules that are relatively imported. + */ + private lookupScopes(node: ts.ClassDeclaration, ngModuleImportedFrom: string|null): + SelectorScopes { + let data: ModuleData|null = null; + + // Either this module was analyzed directly, or has a precompiled ngModuleDef. + if (this._moduleToData.has(node)) { + // The module was analyzed before, and thus its data is available. + data = this._moduleToData.get(node) !; + } else { + // The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type + // annotation that specifies the needed metadata. + if (ngModuleImportedFrom === null) { + // TODO(alxhub): handle hand-compiled ngModuleDef in the current Program. + throw new Error(`Need to read .d.ts module but ngModuleImportedFrom is unspecified`); + } + data = this._readMetadataFromCompiledClass(node, ngModuleImportedFrom); + // Note that data here could still be null, if the class didn't have a precompiled + // ngModuleDef. + } + + if (data === null) { + throw new Error(`Module not registered: ${node.name!.text}`); + } + + return { + compilation: [ + ...data.declarations, + // Expand imports to the exported scope of those imports. + ...flatten(data.imports.map( + ref => this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref)) + .exported)), + // And include the compilation scope of exported modules. + ...flatten( + data.exports.filter(ref => this._moduleToData.has(ref.node as ts.ClassDeclaration)) + .map( + ref => + this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref)) + .exported)) + ], + exported: flatten(data.exports.map(ref => { + if (this._moduleToData.has(ref.node as ts.ClassDeclaration)) { + return this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref)) + .exported; + } else { + return [ref]; + } + })), + }; + } + + /** + * Lookup the selector of a component or directive class. + * + * Potentially this class is declared in a .d.ts file or otherwise has a manually created + * ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read + * to determine the selector. + */ + private lookupDirectiveSelector(node: ts.ClassDeclaration): string|null { + if (this._directiveToSelector.has(node)) { + return this._directiveToSelector.get(node) !; + } else { + return this._readSelectorFromCompiledClass(node); + } + } + + private lookupPipeName(node: ts.ClassDeclaration): string|undefined { + return this._pipeToName.get(node); + } + + /** + * Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts + * file, or in a .ts file with a handwritten definition). + * + * @param clazz the class of interest + * @param ngModuleImportedFrom module specifier of the import path to assume for all declarations + * stemming from this module. + */ + private _readMetadataFromCompiledClass(clazz: ts.ClassDeclaration, ngModuleImportedFrom: string): + ModuleData|null { + // This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`. + // TODO(alxhub): investigate caching of .d.ts module metadata. + const ngModuleDef = reflectStaticField(clazz, 'ngModuleDef'); + if (ngModuleDef === null) { + return null; + } else if ( + // Validate that the shape of the ngModuleDef type is correct. + ngModuleDef.type === undefined || !ts.isTypeReferenceNode(ngModuleDef.type) || + ngModuleDef.type.typeArguments === undefined || + ngModuleDef.type.typeArguments.length !== 4) { + return null; + } + + // Read the ModuleData out of the type arguments. + const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments; + return { + declarations: this._extractReferencesFromType(declarationMetadata, ngModuleImportedFrom), + exports: this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom), + imports: this._extractReferencesFromType(importMetadata, ngModuleImportedFrom), + }; + } + + /** + * Get the selector from type metadata for a class with a precompiled ngComponentDef or + * ngDirectiveDef. + */ + private _readSelectorFromCompiledClass(clazz: ts.ClassDeclaration): string|null { + const def = + reflectStaticField(clazz, 'ngComponentDef') || reflectStaticField(clazz, 'ngDirectiveDef'); + if (def === null) { + // No definition could be found. + return null; + } else if ( + def.type === undefined || !ts.isTypeReferenceNode(def.type) || + def.type.typeArguments === undefined || def.type.typeArguments.length !== 2) { + // The type metadata was the wrong shape. + return null; + } + const type = def.type.typeArguments[1]; + if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { + // The type metadata was the wrong type. + return null; + } + return type.literal.text; + } + + /** + * Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to + * them. + * + * This operation assumes that these types should be imported from `ngModuleImportedFrom` unless + * they themselves were imported from another absolute path. + */ + private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string): Reference[] { + if (!ts.isTupleTypeNode(def)) { + return []; + } + return def.elementTypes.map(element => { + if (!ts.isTypeReferenceNode(element)) { + throw new Error(`Expected TypeReferenceNode`); + } + const type = element.typeName; + const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); + const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); + const clazz = node as ts.ClassDeclaration; + return new AbsoluteReference(node, clazz.name !, moduleName, clazz.name !.text); + }); + } +} + +function flatten(array: T[][]): T[] { + return array.reduce((accum, subArray) => { + accum.push(...subArray); + return accum; + }, [] as T[]); +} + +function absoluteModuleName(ref: Reference): string|null { + const name = (ref.node as ts.ClassDeclaration).name !.text; + if (!(ref instanceof AbsoluteReference)) { + return null; + } + return ref.moduleName; +} + +function convertReferenceMap( + map: Map, context: ts.SourceFile): Map { + return new Map(Array.from(map.entries()).map(([selector, ref]): [ + string, Expression + ] => [selector, referenceToExpression(ref, context)])); +} + +function convertScopeToExpressions( + scope: CompilationScope, context: ts.SourceFile): CompilationScope { + const directives = convertReferenceMap(scope.directives, context); + const pipes = convertReferenceMap(scope.pipes, context); + return {directives, pipes}; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts new file mode 100644 index 0000000000..0f3ee2442d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Expression, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {Reference, reflectConstructorParameters} from '../../metadata'; +import {reflectImportedIdentifier} from '../../metadata/src/reflector'; + +export function getConstructorDependencies( + clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] { + const useType: R3DependencyMetadata[] = []; + const ctorParams = (reflectConstructorParameters(clazz, checker) || []); + ctorParams.forEach(param => { + let tokenExpr = param.typeValueExpr; + let optional = false, self = false, skipSelf = false, host = false; + let resolved = R3ResolvedDependencyType.Token; + param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => { + if (dec.name === 'Inject') { + if (dec.args.length !== 1) { + throw new Error(`Unexpected number of arguments to @Inject().`); + } + tokenExpr = dec.args[0]; + } else if (dec.name === 'Optional') { + optional = true; + } else if (dec.name === 'SkipSelf') { + skipSelf = true; + } else if (dec.name === 'Self') { + self = true; + } else if (dec.name === 'Host') { + host = true; + } else if (dec.name === 'Attribute') { + if (dec.args.length !== 1) { + throw new Error(`Unexpected number of arguments to @Attribute().`); + } + tokenExpr = dec.args[0]; + resolved = R3ResolvedDependencyType.Attribute; + } else { + throw new Error(`Unexpected decorator ${dec.name} on parameter.`); + } + }); + if (tokenExpr === null) { + throw new Error( + `No suitable token for parameter ${(param.name as ts.Identifier).text} of class ${clazz.name!.text} with decorators ${param.decorators.map(dec => dec.from + '#' + dec.name).join(',')}`); + } + if (ts.isIdentifier(tokenExpr)) { + const importedSymbol = reflectImportedIdentifier(tokenExpr, checker); + if (importedSymbol !== null && importedSymbol.from === '@angular/core') { + switch (importedSymbol.name) { + case 'ElementRef': + resolved = R3ResolvedDependencyType.ElementRef; + break; + case 'Injector': + resolved = R3ResolvedDependencyType.Injector; + break; + case 'TemplateRef': + resolved = R3ResolvedDependencyType.TemplateRef; + break; + case 'ViewContainerRef': + resolved = R3ResolvedDependencyType.ViewContainerRef; + break; + default: + // Leave as a Token or Attribute. + } + } + } + const token = new WrappedNodeExpr(tokenExpr); + useType.push({token, optional, self, skipSelf, host, resolved}); + }); + return useType; +} + +export function referenceToExpression(ref: Reference, context: ts.SourceFile): Expression { + const exp = ref.toExpression(context); + if (exp === null) { + throw new Error(`Could not refer to ${ts.SyntaxKind[ref.node.kind]}`); + } + return exp; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel new file mode 100644 index 0000000000..a6898c53fb --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -0,0 +1,28 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ts_library( + name = "test_lib", + testonly = 1, + srcs = glob([ + "**/*.ts", + ]), + deps = [ + "//packages:types", + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/annotations", + "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/testing", + ], +) + +jasmine_node_test( + name = "test", + bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], + deps = [ + ":test_lib", + "//tools/testing:node_no_angular", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts new file mode 100644 index 0000000000..a5c28160ed --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver'; +import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {NgModuleDecoratorHandler} from '../src/ng_module'; +import {SelectorScopeRegistry} from '../src/selector_scope'; + +describe('SelectorScopeRegistry', () => { + it('absolute imports work', () => { + const {program} = makeProgram([ + { + name: 'node_modules/@angular/core/index.d.ts', + contents: ` + export interface NgComponentDef {} + export interface NgModuleDef {} + ` + }, + { + name: 'node_modules/some_library/index.d.ts', + contents: ` + import {NgComponentDef, NgModuleDef} from '@angular/core'; + import * as i0 from './component'; + + export declare class SomeModule { + static ngModuleDef: NgModuleDef; + } + + export declare class SomeCmp { + static ngComponentDef: NgComponentDef; + } + ` + }, + { + name: 'node_modules/some_library/component.d.ts', + contents: ` + export declare class SomeCmp {} + ` + }, + { + name: 'entry.ts', + contents: ` + export class ProgramCmp {} + export class ProgramModule {} + ` + }, + ]); + const checker = program.getTypeChecker(); + const ProgramModule = + getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); + const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); + const SomeModule = getDeclaration( + program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration); + expect(ProgramModule).toBeDefined(); + expect(SomeModule).toBeDefined(); + + const registry = new SelectorScopeRegistry(checker); + + registry.registerModule(ProgramModule, { + declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], + exports: [], + imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], + }); + + registry.registerSelector(ProgramCmp, 'program-cmp'); + + const scope = registry.lookupCompilationScope(ProgramCmp) !; + expect(scope).toBeDefined(); + expect(scope.directives).toBeDefined(); + expect(scope.directives.size).toBe(1); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel index 48bc2b903a..4a5c83e87c 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( ]), module_name = "@angular/compiler-cli/src/ngtsc/metadata", deps = [ + "//packages:types", "//packages/compiler", ], ) diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index ecd28cd0f9..49b2efca8d 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -6,5 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator} from './src/reflector'; -export {Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver'; +export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator, reflectNonStaticField, reflectObjectLiteral, reflectStaticField, reflectTypeEntityToDeclaration,} from './src/reflector'; + +export {AbsoluteReference, Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts index 9aa26326cd..4b807bbb3a 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts @@ -236,3 +236,103 @@ export function reflectImportedIdentifier( return {from, name}; } + +export interface DecoratedNode { + element: T; + decorators: Decorator[]; +} + +export function getDecoratedClassElements( + clazz: ts.ClassDeclaration, checker: ts.TypeChecker): DecoratedNode[] { + const decoratedElements: DecoratedNode[] = []; + clazz.members.forEach(element => { + if (element.decorators !== undefined) { + const decorators = element.decorators.map(decorator => reflectDecorator(decorator, checker)) + .filter(decorator => decorator != null) as Decorator[]; + if (decorators.length > 0) { + decoratedElements.push({element, decorators}); + } + } + }); + return decoratedElements; +} + +export function reflectStaticField( + clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null { + return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => { + // Check if the name matches. + if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) { + return false; + } + // Check if the property is static. + if (member.modifiers === undefined || + !member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) { + return false; + } + // Found the field. + return true; + }) || + null; +} + +export function reflectNonStaticField( + clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null { + return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => { + // Check if the name matches. + if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) { + return false; + } + // Check if the property is static. + if (member.modifiers !== undefined && + member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) { + return false; + } + // Found the field. + return true; + }) || + null; +} + +export function reflectTypeEntityToDeclaration( + type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string | null} { + let realSymbol = checker.getSymbolAtLocation(type); + if (realSymbol === undefined) { + throw new Error(`Cannot resolve type entity to symbol`); + } + while (realSymbol.flags & ts.SymbolFlags.Alias) { + realSymbol = checker.getAliasedSymbol(realSymbol); + } + + let node: ts.Declaration|null = null; + if (realSymbol.valueDeclaration !== undefined) { + node = realSymbol.valueDeclaration; + } else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) { + node = realSymbol.declarations[0]; + } else { + throw new Error(`Cannot resolve type entity symbol to declaration`); + } + + if (ts.isQualifiedName(type)) { + if (!ts.isIdentifier(type.left)) { + throw new Error(`Cannot handle qualified name with non-identifier lhs`); + } + const symbol = checker.getSymbolAtLocation(type.left); + if (symbol === undefined || symbol.declarations === undefined || + symbol.declarations.length !== 1) { + throw new Error(`Cannot resolve qualified type entity lhs to symbol`); + } + const decl = symbol.declarations[0]; + if (ts.isNamespaceImport(decl)) { + const clause = decl.parent !; + const importDecl = clause.parent !; + if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { + throw new Error(`Module specifier is not a string`); + } + return {node, from: importDecl.moduleSpecifier.text}; + } else { + throw new Error(`Unknown import type?`); + } + } else { + return {node, from: null}; + } +} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts index 9f4e4ab200..f97e509cfb 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts @@ -11,8 +11,12 @@ * values where possible and returning a `DynamicValue` signal when not. */ +import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler'; +import * as path from 'path'; import * as ts from 'typescript'; +const TS_DTS_EXTENSION = /(\.d)?\.ts$/; + /** * Represents a value which cannot be determined statically. * @@ -72,24 +76,99 @@ export interface ResolvedValueArray extends Array {} */ type Scope = Map; -/** - * Whether or not to allow references during resolution. - * - * See `StaticInterpreter` for details. - */ -const enum AllowReferences { - No = 0, - Yes = 1, -} - /** * A reference to a `ts.Node`. * * For example, if an expression evaluates to a function or class definition, it will be returned * as a `Reference` (assuming references are allowed in evaluation). */ -export class Reference { +export abstract class Reference { constructor(readonly node: ts.Node) {} + + /** + * Whether an `Expression` can be generated which references the node. + */ + readonly expressable: boolean; + + /** + * Generate an `Expression` representing this type, in the context of the given SourceFile. + * + * This could be a local variable reference, if the symbol is imported, or it could be a new + * import if needed. + */ + abstract toExpression(context: ts.SourceFile): Expression|null; +} + +/** + * A reference to a node only, without any ability to get an `Expression` representing that node. + * + * This is used for returning references to things like method declarations, which are not directly + * referenceable. + */ +export class NodeReference extends Reference { + toExpression(context: ts.SourceFile): null { return null; } +} + +/** + * A reference to a node which has a `ts.Identifier` and can be resolved to an `Expression`. + * + * Imports generated by `ResolvedReference`s are always relative. + */ +export class ResolvedReference extends Reference { + constructor(node: ts.Node, protected identifier: ts.Identifier) { super(node); } + + readonly expressable = true; + + toExpression(context: ts.SourceFile): Expression { + if (ts.getOriginalNode(context) === ts.getOriginalNode(this.node).getSourceFile()) { + return new WrappedNodeExpr(this.identifier); + } else { + // Relative import from context -> this.node.getSourceFile(). + // TODO(alxhub): investigate the impact of multiple source roots here. + // TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support. + let relative = + path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName) + .replace(TS_DTS_EXTENSION, ''); + + // path.relative() does not include the leading './'. + if (!relative.startsWith('.')) { + relative = `./${relative}`; + } + + // path.relative() returns the empty string (converted to './' above) if the two paths are the + // same. + if (relative === './') { + // Same file after all. + return new WrappedNodeExpr(this.identifier); + } else { + return new ExternalExpr(new ExternalReference(relative, this.identifier.text)); + } + } + } +} + +/** + * A reference to a node which has a `ts.Identifer` and an expected absolute module name. + * + * An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import + * the module specifier will be an absolute module name, not a relative path. + */ +export class AbsoluteReference extends Reference { + constructor( + node: ts.Node, private identifier: ts.Identifier, readonly moduleName: string, + private symbolName: string) { + super(node); + } + + readonly expressable = true; + + toExpression(context: ts.SourceFile): Expression { + if (ts.getOriginalNode(context) === ts.getOriginalNode(this.node.getSourceFile())) { + return new WrappedNodeExpr(this.identifier); + } else { + return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName)); + } + } } /** @@ -100,9 +179,8 @@ export class Reference { * @returns a `ResolvedValue` representing the resolved value */ export function staticallyResolve(node: ts.Expression, checker: ts.TypeChecker): ResolvedValue { - return new StaticInterpreter( - checker, new Map(), AllowReferences.No) - .visit(node); + return new StaticInterpreter(checker).visit( + node, {absoluteModuleName: null, scope: new Map()}); } interface BinaryOperatorDef { @@ -144,59 +222,67 @@ const UNARY_OPERATORS = new Map any>([ [ts.SyntaxKind.PlusToken, a => +a], [ts.SyntaxKind.ExclamationToken, a => !a] ]); +interface Context { + absoluteModuleName: string|null; + scope: Scope; +} + class StaticInterpreter { - constructor( - private checker: ts.TypeChecker, private scope: Scope, - private allowReferences: AllowReferences) {} + constructor(private checker: ts.TypeChecker) {} - visit(node: ts.Expression): ResolvedValue { return this.visitExpression(node); } + visit(node: ts.Expression, context: Context): ResolvedValue { + return this.visitExpression(node, context); + } - private visitExpression(node: ts.Expression): ResolvedValue { + private visitExpression(node: ts.Expression, context: Context): ResolvedValue { if (node.kind === ts.SyntaxKind.TrueKeyword) { return true; } else if (node.kind === ts.SyntaxKind.FalseKeyword) { return false; } else if (ts.isStringLiteral(node)) { return node.text; + } else if (ts.isNoSubstitutionTemplateLiteral(node)) { + return node.text; } else if (ts.isNumericLiteral(node)) { return parseFloat(node.text); } else if (ts.isObjectLiteralExpression(node)) { - return this.visitObjectLiteralExpression(node); + return this.visitObjectLiteralExpression(node, context); } else if (ts.isIdentifier(node)) { - return this.visitIdentifier(node); + return this.visitIdentifier(node, context); } else if (ts.isPropertyAccessExpression(node)) { - return this.visitPropertyAccessExpression(node); + return this.visitPropertyAccessExpression(node, context); } else if (ts.isCallExpression(node)) { - return this.visitCallExpression(node); + return this.visitCallExpression(node, context); } else if (ts.isConditionalExpression(node)) { - return this.visitConditionalExpression(node); + return this.visitConditionalExpression(node, context); } else if (ts.isPrefixUnaryExpression(node)) { - return this.visitPrefixUnaryExpression(node); + return this.visitPrefixUnaryExpression(node, context); } else if (ts.isBinaryExpression(node)) { - return this.visitBinaryExpression(node); + return this.visitBinaryExpression(node, context); } else if (ts.isArrayLiteralExpression(node)) { - return this.visitArrayLiteralExpression(node); + return this.visitArrayLiteralExpression(node, context); } else if (ts.isParenthesizedExpression(node)) { - return this.visitParenthesizedExpression(node); + return this.visitParenthesizedExpression(node, context); } else if (ts.isElementAccessExpression(node)) { - return this.visitElementAccessExpression(node); + return this.visitElementAccessExpression(node, context); } else if (ts.isAsExpression(node)) { - return this.visitExpression(node.expression); + return this.visitExpression(node.expression, context); } else if (ts.isNonNullExpression(node)) { - return this.visitExpression(node.expression); + return this.visitExpression(node.expression, context); } else if (ts.isClassDeclaration(node)) { - return this.visitDeclaration(node); + return this.visitDeclaration(node, context); } else { return DYNAMIC_VALUE; } } - private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression): ResolvedValue { + private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression, context: Context): + ResolvedValue { const array: ResolvedValueArray = []; for (let i = 0; i < node.elements.length; i++) { const element = node.elements[i]; if (ts.isSpreadElement(element)) { - const spread = this.visitExpression(element.expression); + const spread = this.visitExpression(element.expression, context); if (isDynamicValue(spread)) { return DYNAMIC_VALUE; } @@ -206,7 +292,7 @@ class StaticInterpreter { array.push(...spread); } else { - const result = this.visitExpression(element); + const result = this.visitExpression(element, context); if (isDynamicValue(result)) { return DYNAMIC_VALUE; } @@ -217,27 +303,28 @@ class StaticInterpreter { return array; } - private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression): ResolvedValue { + private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: Context): + ResolvedValue { const map: ResolvedValueMap = new Map(); for (let i = 0; i < node.properties.length; i++) { const property = node.properties[i]; if (ts.isPropertyAssignment(property)) { - const name = this.stringNameFromPropertyName(property.name); + const name = this.stringNameFromPropertyName(property.name, context); // Check whether the name can be determined statically. if (name === undefined) { return DYNAMIC_VALUE; } - map.set(name, this.visitExpression(property.initializer)); + map.set(name, this.visitExpression(property.initializer, context)); } else if (ts.isShorthandPropertyAssignment(property)) { const symbol = this.checker.getShorthandAssignmentValueSymbol(property); if (symbol === undefined || symbol.valueDeclaration === undefined) { return DYNAMIC_VALUE; } - map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration)); + map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration, context)); } else if (ts.isSpreadAssignment(property)) { - const spread = this.visitExpression(property.expression); + const spread = this.visitExpression(property.expression, context); if (isDynamicValue(spread)) { return DYNAMIC_VALUE; } @@ -252,19 +339,34 @@ class StaticInterpreter { return map; } - private visitIdentifier(node: ts.Identifier): ResolvedValue { + private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue { let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(node); if (symbol === undefined) { return DYNAMIC_VALUE; } - const result = this.visitSymbol(symbol); - if (this.allowReferences === AllowReferences.Yes && isDynamicValue(result)) { - return new Reference(node); - } - return result; + return this.visitSymbol(symbol, context); } - private visitSymbol(symbol: ts.Symbol): ResolvedValue { + private visitSymbol(symbol: ts.Symbol, context: Context): ResolvedValue { + let absoluteModuleName = context.absoluteModuleName; + if (symbol.declarations !== undefined && symbol.declarations.length > 0) { + for (let i = 0; i < symbol.declarations.length; i++) { + const decl = symbol.declarations[i]; + if (ts.isImportSpecifier(decl) && decl.parent !== undefined && + decl.parent.parent !== undefined && decl.parent.parent.parent !== undefined) { + const importDecl = decl.parent.parent.parent; + if (ts.isStringLiteral(importDecl.moduleSpecifier)) { + const moduleSpecifier = importDecl.moduleSpecifier.text; + if (!moduleSpecifier.startsWith('.')) { + absoluteModuleName = moduleSpecifier; + } + } + } + } + } + + const newContext = {...context, absoluteModuleName}; + while (symbol.flags & ts.SymbolFlags.Alias) { symbol = this.checker.getAliasedSymbol(symbol); } @@ -274,42 +376,44 @@ class StaticInterpreter { } if (symbol.valueDeclaration !== undefined) { - return this.visitDeclaration(symbol.valueDeclaration); + return this.visitDeclaration(symbol.valueDeclaration, newContext); } return symbol.declarations.reduce((prev, decl) => { if (!(isDynamicValue(prev) || prev instanceof Reference)) { return prev; } - return this.visitDeclaration(decl); + return this.visitDeclaration(decl, newContext); }, DYNAMIC_VALUE); } - private visitDeclaration(node: ts.Declaration): ResolvedValue { + private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue { if (ts.isVariableDeclaration(node)) { if (!node.initializer) { return undefined; } - return this.visitExpression(node.initializer); - } else if (ts.isParameter(node) && this.scope.has(node)) { - return this.scope.get(node) !; + return this.visitExpression(node.initializer, context); + } else if (ts.isParameter(node) && context.scope.has(node)) { + return context.scope.get(node) !; } else if (ts.isExportAssignment(node)) { - return this.visitExpression(node.expression); + return this.visitExpression(node.expression, context); } else if (ts.isSourceFile(node)) { - return this.visitSourceFile(node); + return this.visitSourceFile(node, context); + } else { + return this.getReference(node, context); } - return this.allowReferences === AllowReferences.Yes ? new Reference(node) : DYNAMIC_VALUE; } - private visitElementAccessExpression(node: ts.ElementAccessExpression): ResolvedValue { - const lhs = this.withReferences.visitExpression(node.expression); + private visitElementAccessExpression(node: ts.ElementAccessExpression, context: Context): + ResolvedValue { + const lhs = this.visitExpression(node.expression, context); if (node.argumentExpression === undefined) { throw new Error(`Expected argument in ElementAccessExpression`); } if (isDynamicValue(lhs)) { return DYNAMIC_VALUE; } - const rhs = this.withNoReferences.visitExpression(node.argumentExpression); + const rhs = this.visitExpression(node.argumentExpression, context); if (isDynamicValue(rhs)) { return DYNAMIC_VALUE; } @@ -318,33 +422,34 @@ class StaticInterpreter { `ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`); } - return this.accessHelper(lhs, rhs); + return this.accessHelper(lhs, rhs, context); } - private visitPropertyAccessExpression(node: ts.PropertyAccessExpression): ResolvedValue { - const lhs = this.withReferences.visitExpression(node.expression); + private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context): + ResolvedValue { + const lhs = this.visitExpression(node.expression, context); const rhs = node.name.text; // TODO: handle reference to class declaration. if (isDynamicValue(lhs)) { return DYNAMIC_VALUE; } - return this.accessHelper(lhs, rhs); + return this.accessHelper(lhs, rhs, context); } - private visitSourceFile(node: ts.SourceFile): ResolvedValue { + private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue { const map = new Map(); const symbol = this.checker.getSymbolAtLocation(node); if (symbol === undefined) { return DYNAMIC_VALUE; } const exports = this.checker.getExportsOfModule(symbol); - exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol))); + exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol, context))); return map; } - private accessHelper(lhs: ResolvedValue, rhs: string|number): ResolvedValue { + private accessHelper(lhs: ResolvedValue, rhs: string|number, context: Context): ResolvedValue { const strIndex = `${rhs}`; if (lhs instanceof Map) { if (lhs.has(strIndex)) { @@ -367,16 +472,16 @@ class StaticInterpreter { const ref = lhs.node; if (ts.isClassDeclaration(ref)) { let value: ResolvedValue = undefined; - const member = ref.members.filter(member => isStatic(member)) - .find( - member => member.name !== undefined && - this.stringNameFromPropertyName(member.name) === strIndex); + const member = + ref.members.filter(member => isStatic(member)) + .find( + member => member.name !== undefined && + this.stringNameFromPropertyName(member.name, context) === strIndex); if (member !== undefined) { if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) { - value = this.visitExpression(member.initializer); + value = this.visitExpression(member.initializer, context); } else if (ts.isMethodDeclaration(member)) { - value = this.allowReferences === AllowReferences.Yes ? new Reference(member) : - DYNAMIC_VALUE; + value = new NodeReference(member); } } return value; @@ -385,8 +490,8 @@ class StaticInterpreter { throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`); } - private visitCallExpression(node: ts.CallExpression): ResolvedValue { - const lhs = this.withReferences.visitExpression(node.expression); + private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue { + const lhs = this.visitExpression(node.expression, context); if (!(lhs instanceof Reference)) { throw new Error(`attempting to call something that is not a function: ${lhs}`); } else if (!isFunctionOrMethodDeclaration(lhs.node) || !lhs.node.body) { @@ -406,43 +511,46 @@ class StaticInterpreter { let value: ResolvedValue = undefined; if (index < node.arguments.length) { const arg = node.arguments[index]; - value = this.visitExpression(arg); + value = this.visitExpression(arg, context); } if (value === undefined && param.initializer !== undefined) { - value = this.visitExpression(param.initializer); + value = this.visitExpression(param.initializer, context); } newScope.set(param, value); }); - return ret.expression !== undefined ? this.withScope(newScope).visitExpression(ret.expression) : - undefined; + return ret.expression !== undefined ? + this.visitExpression(ret.expression, {...context, scope: newScope}) : + undefined; } - private visitConditionalExpression(node: ts.ConditionalExpression): ResolvedValue { - const condition = this.withNoReferences.visitExpression(node.condition); + private visitConditionalExpression(node: ts.ConditionalExpression, context: Context): + ResolvedValue { + const condition = this.visitExpression(node.condition, context); if (isDynamicValue(condition)) { return condition; } if (condition) { - return this.visitExpression(node.whenTrue); + return this.visitExpression(node.whenTrue, context); } else { - return this.visitExpression(node.whenFalse); + return this.visitExpression(node.whenFalse, context); } } - private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ResolvedValue { + private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression, context: Context): + ResolvedValue { const operatorKind = node.operator; if (!UNARY_OPERATORS.has(operatorKind)) { throw new Error(`Unsupported prefix unary operator: ${ts.SyntaxKind[operatorKind]}`); } const op = UNARY_OPERATORS.get(operatorKind) !; - const value = this.visitExpression(node.operand); + const value = this.visitExpression(node.operand, context); return isDynamicValue(value) ? DYNAMIC_VALUE : op(value); } - private visitBinaryExpression(node: ts.BinaryExpression): ResolvedValue { + private visitBinaryExpression(node: ts.BinaryExpression, context: Context): ResolvedValue { const tokenKind = node.operatorToken.kind; if (!BINARY_OPERATORS.has(tokenKind)) { throw new Error(`Unsupported binary operator: ${ts.SyntaxKind[tokenKind]}`); @@ -451,44 +559,42 @@ class StaticInterpreter { const opRecord = BINARY_OPERATORS.get(tokenKind) !; let lhs: ResolvedValue, rhs: ResolvedValue; if (opRecord.literal) { - const withNoReferences = this.withNoReferences; - lhs = literal(withNoReferences.visitExpression(node.left)); - rhs = literal(withNoReferences.visitExpression(node.right)); + lhs = literal(this.visitExpression(node.left, context)); + rhs = literal(this.visitExpression(node.right, context)); } else { - lhs = this.visitExpression(node.left); - rhs = this.visitExpression(node.right); + lhs = this.visitExpression(node.left, context); + rhs = this.visitExpression(node.right, context); } return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs); } - private visitParenthesizedExpression(node: ts.ParenthesizedExpression): ResolvedValue { - return this.visitExpression(node.expression); + private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context): + ResolvedValue { + return this.visitExpression(node.expression, context); } - private stringNameFromPropertyName(node: ts.PropertyName): string|undefined { + private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string|undefined { if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { return node.text; } else { // ts.ComputedPropertyName - const literal = this.withNoReferences.visitExpression(node.expression); + const literal = this.visitExpression(node.expression, context); return typeof literal === 'string' ? literal : undefined; } } - private get withReferences(): StaticInterpreter { - return this.allowReferences === AllowReferences.Yes ? - this : - new StaticInterpreter(this.checker, this.scope, AllowReferences.Yes); - } - - private get withNoReferences(): StaticInterpreter { - return this.allowReferences === AllowReferences.No ? - this : - new StaticInterpreter(this.checker, this.scope, AllowReferences.No); - } - - private withScope(scope: Scope): StaticInterpreter { - return new StaticInterpreter(this.checker, scope, this.allowReferences); + private getReference(node: ts.Declaration, context: Context): Reference { + const id = identifierOfDeclaration(node); + if (id === undefined) { + throw new Error(`Don't know how to refer to ${ts.SyntaxKind[node.kind]}`); + } + if (context.absoluteModuleName !== null) { + // TODO(alxhub): investigate whether this can get symbol names wrong in the event of + // re-exports under different names. + return new AbsoluteReference(node, id, context.absoluteModuleName, id.text); + } else { + return new ResolvedReference(node, id); + } } } @@ -512,3 +618,17 @@ function literal(value: ResolvedValue): any { } throw new Error(`Value ${value} is not literal and cannot be used in this context.`); } + +function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined { + if (ts.isClassDeclaration(decl)) { + return decl.name; + } else if (ts.isFunctionDeclaration(decl)) { + return decl.name; + } else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) { + return decl.name; + } else if (ts.isShorthandPropertyAssignment(decl)) { + return decl.name; + } else { + return undefined; + } +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel index 746e88739e..9a675329a2 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/testing", ], diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts index 8f6feddf27..5498f4aa57 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {ExternalExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; -import {ResolvedValue, staticallyResolve} from '../src/resolver'; +import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver'; function makeSimpleProgram(contents: string): ts.Program { return makeProgram([{name: 'entry.ts', contents}]).program; @@ -116,6 +117,62 @@ describe('ngtsc metadata', () => { expect(evaluate(`const x = 3;`, '!!x')).toEqual(true); }); + it('imports work', () => { + const {program} = makeProgram([ + {name: 'second.ts', contents: 'export function foo(bar) { return bar; }'}, + { + name: 'entry.ts', + contents: ` + import {foo} from './second'; + const target$ = foo; + ` + }, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); + const expr = result.initializer !; + const resolved = staticallyResolve(expr, checker); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); + expect(resolved.expressable).toBe(true); + const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); + if (!(reference instanceof ExternalExpr)) { + return fail('Expected expression reference to be an external (import) expression'); + } + expect(reference.value.moduleName).toBe('./second'); + expect(reference.value.name).toBe('foo'); + }); + + it('absolute imports work', () => { + const {program} = makeProgram([ + {name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'}, + { + name: 'entry.ts', + contents: ` + import {foo} from 'some_library'; + const target$ = foo; + ` + }, + ]); + const checker = program.getTypeChecker(); + const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); + const expr = result.initializer !; + const resolved = staticallyResolve(expr, checker); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); + expect(resolved.expressable).toBe(true); + const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); + if (!(reference instanceof ExternalExpr)) { + return fail('Expected expression reference to be an external (import) expression'); + } + expect(reference.value.moduleName).toBe('some_library'); + expect(reference.value.name).toBe('foo'); + }); + it('reads values from default exports', () => { const {program} = makeProgram([ {name: 'second.ts', contents: 'export default {property: "test"}'}, @@ -130,7 +187,6 @@ describe('ngtsc metadata', () => { const checker = program.getTypeChecker(); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - debugger; expect(staticallyResolve(expr, checker)).toEqual('test'); }); diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 579a415f02..0df33b4627 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -12,8 +12,9 @@ import * as ts from 'typescript'; import * as api from '../transformers/api'; +import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, SelectorScopeRegistry} from './annotations'; import {CompilerHost} from './compiler_host'; -import {InjectableCompilerAdapter, IvyCompilation, ivyTransformFactory} from './transform'; +import {IvyCompilation, ivyTransformFactory} from './transform'; export class NgtscProgram implements api.Program { private tsProgram: ts.Program; @@ -89,10 +90,16 @@ export class NgtscProgram implements api.Program { const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults; const checker = this.tsProgram.getTypeChecker(); + const scopeRegistry = new SelectorScopeRegistry(checker); // Set up the IvyCompilation, which manages state for the Ivy transformer. - const adapters = [new InjectableCompilerAdapter(checker)]; - const compilation = new IvyCompilation(adapters, checker); + const handlers = [ + new ComponentDecoratorHandler(checker, scopeRegistry), + new DirectiveDecoratorHandler(checker, scopeRegistry), + new InjectableDecoratorHandler(checker), + new NgModuleDecoratorHandler(checker, scopeRegistry), + ]; + const compilation = new IvyCompilation(handlers, checker); // Analyze every source file in the program. this.tsProgram.getSourceFiles() diff --git a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts index ad9356834a..aea082817c 100644 --- a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts +++ b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts @@ -15,7 +15,10 @@ export function makeProgram(files: {name: string, contents: string}[]): files.forEach(file => host.writeFile(file.name, file.contents)); const rootNames = files.map(file => host.getCanonicalFileName(file.name)); - const program = ts.createProgram(rootNames, {noLib: true, experimentalDecorators: true}, host); + const program = ts.createProgram( + rootNames, + {noLib: true, experimentalDecorators: true, moduleResolution: ts.ModuleResolutionKind.NodeJs}, + host); const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()]; if (diags.length > 0) { throw new Error( diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 61232d4328..ab5918688c 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -12,5 +12,6 @@ ts_library( deps = [ "//packages/compiler", "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/util", ], ) diff --git a/packages/compiler-cli/src/ngtsc/transform/index.ts b/packages/compiler-cli/src/ngtsc/transform/index.ts index bf776e1a2d..ccf8eff5bd 100644 --- a/packages/compiler-cli/src/ngtsc/transform/index.ts +++ b/packages/compiler-cli/src/ngtsc/transform/index.ts @@ -6,6 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ +export * from './src/api'; export {IvyCompilation} from './src/compilation'; -export {InjectableCompilerAdapter} from './src/injectable'; export {ivyTransformFactory} from './src/transform'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index e1b63c19e0..4ce4734a9d 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, Type} from '@angular/compiler'; +import {Expression, Statement, Type} from '@angular/compiler'; import * as ts from 'typescript'; import {Decorator} from '../../metadata'; @@ -15,13 +15,13 @@ import {Decorator} from '../../metadata'; * Provides the interface between a decorator compiler from @angular/compiler and the Typescript * compiler/transform. * - * The decorator compilers in @angular/compiler do not depend on Typescript. The adapter is + * The decorator compilers in @angular/compiler do not depend on Typescript. The handler is * responsible for extracting the information required to perform compilation from the decorators * and Typescript source, invoking the decorator compiler, and returning the result. */ -export interface CompilerAdapter { +export interface DecoratorHandler { /** - * Scan a set of reflected decorators and determine if this adapter is responsible for compilation + * Scan a set of reflected decorators and determine if this handler is responsible for compilation * of one of them. */ detect(decorator: Decorator[]): Decorator|undefined; @@ -37,7 +37,7 @@ export interface CompilerAdapter { * Generate a description of the field which should be added to the class, including any * initialization code to be generated. */ - compile(node: ts.ClassDeclaration, analysis: A): AddStaticFieldInstruction; + compile(node: ts.ClassDeclaration, analysis: A): CompileResult; } /** @@ -54,8 +54,9 @@ export interface AnalysisOutput { * A description of the static field to add to a class, including an initialization expression * and a type for the .d.ts file. */ -export interface AddStaticFieldInstruction { +export interface CompileResult { field: string; initializer: Expression; + statements: Statement[]; type: Type; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 45e39f552b..948a01ba6f 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -11,17 +11,18 @@ import * as ts from 'typescript'; import {Decorator, reflectDecorator} from '../../metadata'; -import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from './api'; import {DtsFileTransformer} from './declaration'; import {ImportManager, translateType} from './translator'; + /** * Record of an adapter which decided to emit a static field, and the analysis it performed to * prepare for that operation. */ interface EmitFieldOperation { - adapter: CompilerAdapter; + adapter: DecoratorHandler; analysis: AnalysisOutput; decorator: ts.Decorator; } @@ -44,7 +45,7 @@ export class IvyCompilation { */ private dtsMap = new Map(); - constructor(private adapters: CompilerAdapter[], private checker: ts.TypeChecker) {} + constructor(private handlers: DecoratorHandler[], private checker: ts.TypeChecker) {} /** * Analyze a source file and produce diagnostics for it (if any). @@ -60,8 +61,8 @@ export class IvyCompilation { node.decorators.map(decorator => reflectDecorator(decorator, this.checker)) .filter(decorator => decorator !== null) as Decorator[]; - // Look through the CompilerAdapters to see if any are relevant. - this.adapters.forEach(adapter => { + // Look through the DecoratorHandlers to see if any are relevant. + this.handlers.forEach(adapter => { // An adapter is relevant if it matches one of the decorators on the class. const decorator = adapter.detect(decorators); if (decorator === undefined) { @@ -101,7 +102,7 @@ export class IvyCompilation { * Perform a compilation operation on the given class declaration and return instructions to an * AST transformer if any are available. */ - compileIvyFieldFor(node: ts.ClassDeclaration): AddStaticFieldInstruction|undefined { + compileIvyFieldFor(node: ts.ClassDeclaration): CompileResult|undefined { // Look to see whether the original node was analyzed. If not, there's nothing to do. const original = ts.getOriginalNode(node) as ts.ClassDeclaration; if (!this.analysis.has(original)) { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index 1e2c3d4abb..3038806287 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AddStaticFieldInstruction} from './api'; +import {CompileResult} from './api'; import {ImportManager, translateType} from './translator'; @@ -17,15 +17,13 @@ import {ImportManager, translateType} from './translator'; * Processes .d.ts file text and adds static field declarations, with types. */ export class DtsFileTransformer { - private ivyFields = new Map(); + private ivyFields = new Map(); private imports = new ImportManager(); /** * Track that a static field was added to the code for a class. */ - recordStaticField(name: string, decl: AddStaticFieldInstruction): void { - this.ivyFields.set(name, decl); - } + recordStaticField(name: string, decl: CompileResult): void { this.ivyFields.set(name, decl); } /** * Process the .d.ts text for a file and add any declarations which were recorded. diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 85d6fcb9c1..e13a887e65 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -9,8 +9,10 @@ import {WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; +import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor'; + import {IvyCompilation} from './compilation'; -import {ImportManager, translateExpression} from './translator'; +import {ImportManager, translateExpression, translateStatement} from './translator'; export function ivyTransformFactory(compilation: IvyCompilation): ts.TransformerFactory { @@ -21,6 +23,40 @@ export function ivyTransformFactory(compilation: IvyCompilation): }; } +class IvyVisitor extends Visitor { + constructor(private compilation: IvyCompilation, private importManager: ImportManager) { + super(); + } + + visitClassDeclaration(node: ts.ClassDeclaration): + VisitListEntryResult { + // Determine if this class has an Ivy field that needs to be added, and compile the field + // to an expression if so. + const res = this.compilation.compileIvyFieldFor(node); + if (res !== undefined) { + // There is a field to add. Translate the initializer for the field into TS nodes. + const exprNode = translateExpression(res.initializer, this.importManager); + + // Create a static property declaration for the new field. + const property = ts.createProperty( + undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined, + exprNode); + + // Replace the class declaration with an updated version. + node = ts.updateClassDeclaration( + node, + // Remove the decorator which triggered this compilation, leaving the others alone. + maybeFilterDecorator(node.decorators, this.compilation.ivyDecoratorFor(node) !), + node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], + [...node.members, property]); + const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager)); + return {node, before: statements}; + } + + return {node}; + } +} + /** * A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`. */ @@ -30,7 +66,7 @@ function transformIvySourceFile( const importManager = new ImportManager(); // Recursively scan through the AST and perform any updates requested by the IvyCompilation. - const sf = visitNode(file); + const sf = visit(file, new IvyVisitor(compilation, importManager), context); // Generate the import statements to prepend. const imports = importManager.getAllImports().map( @@ -44,44 +80,8 @@ function transformIvySourceFile( sf.statements = ts.createNodeArray([...imports, ...sf.statements]); } return sf; - - // Helper function to process a class declaration. - function visitClassDeclaration(node: ts.ClassDeclaration): ts.ClassDeclaration { - // Determine if this class has an Ivy field that needs to be added, and compile the field - // to an expression if so. - const res = compilation.compileIvyFieldFor(node); - if (res !== undefined) { - // There is a field to add. Translate the initializer for the field into TS nodes. - const exprNode = translateExpression(res.initializer, importManager); - - // Create a static property declaration for the new field. - const property = ts.createProperty( - undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined, - exprNode); - - // Replace the class declaration with an updated version. - node = ts.updateClassDeclaration( - node, - // Remove the decorator which triggered this compilation, leaving the others alone. - maybeFilterDecorator(node.decorators, compilation.ivyDecoratorFor(node) !), - node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], - [...node.members, property]); - } - - // Recurse into the class declaration in case there are nested class declarations. - return ts.visitEachChild(node, child => visitNode(child), context); - } - - // Helper function that recurses through the nodes and processes each one. - function visitNode(node: T): T; - function visitNode(node: ts.Node): ts.Node { - if (ts.isClassDeclaration(node)) { - return visitClassDeclaration(node); - } else { - return ts.visitEachChild(node, child => visitNode(child), context); - } - } } + function maybeFilterDecorator( decorators: ts.NodeArray| undefined, toRemove: ts.Decorator): ts.NodeArray|undefined { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts index 77151a94ba..69ad563a71 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts @@ -6,9 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ -import {ArrayType, AssertNotNull, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; import * as ts from 'typescript'; +const BINARY_OPERATORS = new Map([ + [BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken], + [BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken], + [BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken], + [BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken], + [BinaryOperator.Divide, ts.SyntaxKind.SlashToken], + [BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken], + [BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken], + [BinaryOperator.Lower, ts.SyntaxKind.LessThanToken], + [BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken], + [BinaryOperator.Minus, ts.SyntaxKind.MinusToken], + [BinaryOperator.Modulo, ts.SyntaxKind.PercentToken], + [BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken], + [BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken], + [BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken], + [BinaryOperator.Or, ts.SyntaxKind.BarBarToken], + [BinaryOperator.Plus, ts.SyntaxKind.PlusToken], +]); + export class ImportManager { private moduleToIndex = new Map(); private nextIndex = 0; @@ -32,6 +51,10 @@ export function translateExpression(expression: Expression, imports: ImportManag return expression.visitExpression(new ExpressionTranslatorVisitor(imports), null); } +export function translateStatement(statement: Statement, imports: ImportManager): ts.Statement { + return statement.visitStatement(new ExpressionTranslatorVisitor(imports), null); +} + export function translateType(type: Type, imports: ImportManager): string { return type.visitType(new TypeTranslatorVisitor(imports), null); } @@ -39,16 +62,23 @@ export function translateType(type: Type, imports: ImportManager): string { class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor { constructor(private imports: ImportManager) {} - visitDeclareVarStmt(stmt: DeclareVarStmt, context: any) { - throw new Error('Method not implemented.'); + visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): ts.VariableStatement { + return ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList([ts.createVariableDeclaration( + stmt.name, undefined, stmt.value && stmt.value.visitExpression(this, context))])); } - visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) { - throw new Error('Method not implemented.'); + visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): ts.FunctionDeclaration { + return ts.createFunctionDeclaration( + undefined, undefined, undefined, stmt.name, undefined, + stmt.params.map(param => ts.createParameter(undefined, undefined, undefined, param.name)), + undefined, + ts.createBlock(stmt.statements.map(child => child.visitStatement(this, context)))); } - visitExpressionStmt(stmt: ExpressionStatement, context: any) { - throw new Error('Method not implemented.'); + visitExpressionStmt(stmt: ExpressionStatement, context: any): ts.ExpressionStatement { + return ts.createStatement(stmt.expr.visitExpression(this, context)); } visitReturnStmt(stmt: ReturnStatement, context: any): ts.ReturnStatement { @@ -59,7 +89,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor throw new Error('Method not implemented.'); } - visitIfStmt(stmt: IfStmt, context: any) { throw new Error('Method not implemented.'); } + visitIfStmt(stmt: IfStmt, context: any): ts.IfStatement { + return ts.createIf( + stmt.condition.visitExpression(this, context), + ts.createBlock(stmt.trueCase.map(child => child.visitStatement(this, context))), + stmt.falseCase.length > 0 ? + ts.createBlock(stmt.falseCase.map(child => child.visitStatement(this, context))) : + undefined); + } visitTryCatchStmt(stmt: TryCatchStmt, context: any) { throw new Error('Method not implemented.'); @@ -89,12 +126,17 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor throw new Error('Method not implemented.'); } - visitWritePropExpr(expr: WritePropExpr, context: any): never { - throw new Error('Method not implemented.'); + visitWritePropExpr(expr: WritePropExpr, context: any): ts.BinaryExpression { + return ts.createBinary( + ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name), + ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context)); } - visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never { - throw new Error('Method not implemented.'); + visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): ts.CallExpression { + const target = ast.receiver.visitExpression(this, context); + return ts.createCall( + ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined, + ast.args.map(arg => arg.visitExpression(this, context))); } visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): ts.CallExpression { @@ -156,20 +198,26 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context)))); } - visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): never { - throw new Error('Method not implemented.'); + visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): ts.Expression { + if (!BINARY_OPERATORS.has(ast.operator)) { + throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`); + } + const binEx = ts.createBinary( + ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator) !, + ast.rhs.visitExpression(this, context)); + return ast.parens ? ts.createParen(binEx) : binEx; } - visitReadPropExpr(ast: ReadPropExpr, context: any): never { - throw new Error('Method not implemented.'); + visitReadPropExpr(ast: ReadPropExpr, context: any): ts.PropertyAccessExpression { + return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name); } visitReadKeyExpr(ast: ReadKeyExpr, context: any): never { throw new Error('Method not implemented.'); } - visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): never { - throw new Error('Method not implemented.'); + visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): ts.ArrayLiteralExpression { + return ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context))); } visitLiteralMapExpr(ast: LiteralMapExpr, context: any): ts.ObjectLiteralExpression { @@ -297,8 +345,9 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { visitReadKeyExpr(ast: ReadKeyExpr, context: any) { throw new Error('Method not implemented.'); } - visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any) { - throw new Error('Method not implemented.'); + visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): string { + const values = ast.entries.map(expr => expr.visitExpression(this, context)); + return `[${values.join(',')}]`; } visitLiteralMapExpr(ast: LiteralMapExpr, context: any) { diff --git a/packages/compiler-cli/src/ngtsc/util/src/visitor.ts b/packages/compiler-cli/src/ngtsc/util/src/visitor.ts index f3e04a1056..455a1d3613 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/visitor.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/visitor.ts @@ -33,7 +33,7 @@ export abstract class Visitor { /** * Maps statements to an array of statements that should be inserted before them. */ - private _before = new Map(); + private _before = new Map(); /** * Visit a class declaration, returning at least the transformed declaration and optionally other @@ -44,16 +44,15 @@ export abstract class Visitor { return {node}; } - private _visitClassDeclaration(node: ts.ClassDeclaration, context: ts.TransformationContext): - ts.ClassDeclaration { - const result = this.visitClassDeclaration(node); - const visited = ts.visitEachChild(result.node, child => this._visit(child, context), context); + private _visitListEntryNode( + node: T, visitor: (node: T) => VisitListEntryResult): T { + const result = visitor(node); if (result.before !== undefined) { // Record that some nodes should be inserted before the given declaration. The declaration's // parent's _visit call is responsible for performing this insertion. - this._before.set(visited, result.before); + this._before.set(result.node, result.before); } - return visited; + return result.node; } /** @@ -61,11 +60,6 @@ export abstract class Visitor { */ visitOtherNode(node: T): T { return node; } - private _visitOtherNode(node: T, context: ts.TransformationContext): T { - return ts.visitEachChild( - this.visitOtherNode(node), child => this._visit(child, context), context); - } - /** * @internal */ @@ -73,10 +67,14 @@ export abstract class Visitor { // First, visit the node. visitedNode starts off as `null` but should be set after visiting // is completed. let visitedNode: T|null = null; + + node = ts.visitEachChild(node, child => this._visit(child, context), context) as T; + if (ts.isClassDeclaration(node)) { - visitedNode = this._visitClassDeclaration(node, context) as typeof node; + visitedNode = this._visitListEntryNode( + node, (node: ts.ClassDeclaration) => this.visitClassDeclaration(node)) as typeof node; } else { - visitedNode = this._visitOtherNode(node, context); + visitedNode = this.visitOtherNode(node); } // If the visited node has a `statements` array then process them, maybe replacing the visited diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index c7d1c3c5b3..a87052c3a1 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -16,6 +16,8 @@ function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> { return null !; } +export const Component = callableClassDecorator(); +export const Directive = callableClassDecorator(); export const Injectable = callableClassDecorator(); export const NgModule = callableClassDecorator(); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 4c56ce857d..b4869e1728 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -95,7 +95,7 @@ describe('ngtsc behavioral tests', () => { }`); }); - it('should compile without errors', () => { + it('should compile Injectables without errors', () => { writeConfig(); write('test.ts', ` import {Injectable} from '@angular/core'; @@ -122,4 +122,62 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef;'); expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef;'); }); + + it('should compile Components without errors', () => { + writeConfig(); + write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: 'this is a test', + }) + export class TestCmp {} + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const jsContents = getContents('test.js'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵdefineComponent'); + expect(jsContents).not.toContain('__decorate'); + + const dtsContents = getContents('test.d.ts'); + expect(dtsContents).toContain('static ngComponentDef: i0.ComponentDef'); + }); + + it('should compile NgModules without errors', () => { + writeConfig(); + write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: 'this is a test', + }) + export class TestCmp {} + + @NgModule({ + declarations: [TestCmp], + }) + export class TestModule {} + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const jsContents = getContents('test.js'); + expect(jsContents) + .toContain( + 'i0.ɵdefineNgModule({ type: TestModule, bootstrap: [], ' + + 'declarations: [TestCmp], imports: [], exports: [] })'); + + const dtsContents = getContents('test.d.ts'); + expect(dtsContents).toContain('static ngComponentDef: i0.ComponentDef'); + expect(dtsContents) + .toContain('static ngModuleDef: i0.NgModuleDef'); + expect(dtsContents).not.toContain('__decorate'); + }); }); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 51dc0bcafe..03f6f8e5fe 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -119,6 +119,11 @@ export class Identifiers { moduleName: CORE, }; + static NgModuleDef: o.ExternalReference = { + name: 'NgModuleDef', + moduleName: CORE, + }; + static defineNgModule: o.ExternalReference = {name: 'ɵdefineNgModule', moduleName: CORE}; static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts index 91a365a3dc..78f3e0db84 100644 --- a/packages/compiler/src/render3/r3_module_compiler.ts +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -72,8 +72,11 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { exports: o.literalArr(exports), })]); - // TODO(alxhub): write a proper type reference when AOT compilation of @NgModule is implemented. - const type = new o.ExpressionType(o.NULL_EXPR); + const type = new o.ExpressionType(o.importExpr(R3.NgModuleDef, [ + new o.ExpressionType(moduleType), new o.ExpressionType(o.literalArr(declarations)), + new o.ExpressionType(o.literalArr(imports)), new o.ExpressionType(o.literalArr(exports)) + ])); + const additionalStatements: o.Statement[] = []; return {expression, type, additionalStatements}; } diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 089b652a7c..c2cb6551ac 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -85,8 +85,9 @@ export function compileDirectiveFromMetadata( bindingParser: BindingParser): R3DirectiveDef { const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser); const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]); - const type = - new o.ExpressionType(o.importExpr(R3.DirectiveDef, [new o.ExpressionType(meta.type)])); + const type = new o.ExpressionType(o.importExpr( + R3.DirectiveDef, + [new o.ExpressionType(meta.type), new o.ExpressionType(o.literal(meta.selector || ''))])); return {expression, type}; } @@ -154,8 +155,9 @@ export function compileComponentFromMetadata( } const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]); - const type = - new o.ExpressionType(o.importExpr(R3.ComponentDef, [new o.ExpressionType(meta.type)])); + const type = new o.ExpressionType(o.importExpr( + R3.ComponentDef, + [new o.ExpressionType(meta.type), new o.ExpressionType(o.literal(meta.selector || ''))])); return {expression, type}; } @@ -243,15 +245,15 @@ function directiveMetadataFromGlobalMetadata( selector: directive.selector, deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector), queries: queriesFromGlobalMetadata(directive.queries, outputCtx), + lifecycle: { + usesOnChanges: + directive.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges), + }, host: { attributes: directive.hostAttributes, listeners: summary.hostListeners, properties: summary.hostProperties, }, - lifecycle: { - usesOnChanges: - directive.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges), - }, inputs: directive.inputs, outputs: directive.outputs, }; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 57ab502aa6..decfe1ea94 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -849,7 +849,7 @@ function interpolate(args: o.Expression[]): o.Expression { * @param templateUrl URL to use for source mapping of the parsed template */ export function parseTemplate( - template: string, templateUrl: string, options: {preserveWhitespace?: boolean} = {}): + template: string, templateUrl: string, options: {preserveWhitespaces?: boolean} = {}): {errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} { const bindingParser = makeBindingParser(); const htmlParser = new HtmlParser(); @@ -860,7 +860,7 @@ export function parseTemplate( } let rootNodes: html.Node[] = parseResult.rootNodes; - if (!options.preserveWhitespace) { + if (!options.preserveWhitespaces) { rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes); } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index c944bace23..11e7feb6a9 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -11,6 +11,7 @@ export { defineComponent as ɵdefineComponent, defineDirective as ɵdefineDirective, definePipe as ɵdefinePipe, + defineNgModule as ɵdefineNgModule, detectChanges as ɵdetectChanges, renderComponent as ɵrenderComponent, ComponentType as ɵComponentType, @@ -82,8 +83,8 @@ export { st as ɵst, ld as ɵld, Pp as ɵPp, - ComponentDef as ɵComponentDef, - DirectiveDef as ɵDirectiveDef, + ComponentDefInternal as ɵComponentDef, + DirectiveDefInternal as ɵDirectiveDef, PipeDef as ɵPipeDef, whenRendered as ɵwhenRendered, } from './render3/index'; diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 2866829128..e606c12fa4 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -13,7 +13,7 @@ import {Attribute, ContentChild, ContentChildren, Query, ViewChild, ViewChildren} from './metadata/di'; import {Component, Directive, HostBinding, HostListener, Input, Output, Pipe} from './metadata/directives'; -import {ModuleWithProviders, NgModule, NgModuleDef, SchemaMetadata, defineNgModule} from './metadata/ng_module'; +import {ModuleWithProviders, NgModule, SchemaMetadata} from './metadata/ng_module'; import {ViewEncapsulation} from './metadata/view'; export {ANALYZE_FOR_ENTRY_COMPONENTS, Attribute, ContentChild, ContentChildDecorator, ContentChildren, ContentChildrenDecorator, Query, ViewChild, ViewChildDecorator, ViewChildren, ViewChildrenDecorator} from './metadata/di'; diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index 711c1855a4..61e3ef7cfa 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -388,6 +388,14 @@ export interface Directive { * ``` */ queries?: {[key: string]: any}; + + /** + * If true, this directive/component will be skipped by the AOT compiler and so will always be + * compiled using JIT. + * + * This exists to support future Ivy work and has no effect currently. + */ + jit?: true; } /** diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index 40def87bb2..2bc8151c07 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -27,11 +27,39 @@ export interface NgModuleTransitiveScopes { exported: {directives: Set; pipes: Set;}; } -export interface NgModuleDef { +/** + * A version of {@link NgModuleDef} that represents the runtime type shape only, and excludes + * metadata parameters. + */ +export type NgModuleDefInternal = NgModuleDef; + +/** + * Runtime link information for NgModules. + * + * This is the internal data structure used by the runtime to assemble components, directives, + * pipes, and injectors. + * + * NOTE: Always use `defineNgModule` function to create this object, + * never create the object directly since the shape of this object + * can change between versions. + */ +export interface NgModuleDef { + /** Token representing the module. Used by DI. */ type: T; + + /** List of components to bootstrap. */ bootstrap: Type[]; + + /** List of components, directives, and pipes declared by this module. */ declarations: Type[]; + + /** List of modules or `ModuleWithProviders` imported by this module. */ imports: Type[]; + + /** + * List of modules, `ModuleWithProviders`, components, directives, or pipes exported by this + * module. + */ exports: Type[]; /** @@ -42,18 +70,6 @@ export interface NgModuleDef { transitiveCompileScopes: NgModuleTransitiveScopes|null; } -export function defineNgModule(def: {type: T} & Partial>): never { - const res: NgModuleDef = { - type: def.type, - bootstrap: def.bootstrap || [], - declarations: def.declarations || [], - imports: def.imports || [], - exports: def.exports || [], - transitiveCompileScopes: null, - }; - return res as never; -} - /** * A wrapper around a module that also includes the providers. * @@ -226,6 +242,14 @@ export interface NgModule { * `getModuleFactory`. */ id?: string; + + /** + * If true, this module will be skipped by the AOT compiler and so will always be compiled + * using JIT. + * + * This exists to support future Ivy work and has no effect currently. + */ + jit?: true; } function preR3NgModuleCompile(moduleType: InjectorType, metadata: NgModule): void { diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 5f275cd7cd..2009bf8f21 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -15,7 +15,7 @@ import {Sanitizer} from '../sanitization/security'; import {assertComponentType, assertDefined} from './assert'; import {queueInitHooks, queueLifecycleHooks} from './hooks'; import {CLEAN_PROMISE, ROOT_DIRECTIVE_INDICES, _getComponentHostLElementNode, baseDirectiveCreate, createLViewData, createTView, detectChangesInternal, enterView, executeInitAndContentHooks, getRootView, hostElement, initChangeDetectorIfExisting, leaveView, locateHostElement, setHostBindings,} from './instructions'; -import {ComponentDef, ComponentType} from './interfaces/definition'; +import {ComponentDef, ComponentDefInternal, ComponentType} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {LViewData, LViewFlags, RootContext, INJECTOR, CONTEXT, TVIEW} from './interfaces/view'; @@ -52,7 +52,7 @@ export interface CreateComponentOptions { * features list because there's no way of knowing when the component will be used as * a root component. */ - hostFeatures?: ((component: T, componentDef: ComponentDef) => void)[]; + hostFeatures?: ((component: T, componentDef: ComponentDef) => void)[]; /** * A function which is used to schedule change detection work in the future. @@ -96,7 +96,8 @@ export function renderComponent( ngDevMode && assertComponentType(componentType); const rendererFactory = opts.rendererFactory || domRendererFactory3; const sanitizer = opts.sanitizer || null; - const componentDef = (componentType as ComponentType).ngComponentDef as ComponentDef; + const componentDef = + (componentType as ComponentType).ngComponentDef as ComponentDefInternal; if (componentDef.type != componentType) componentDef.type = componentType; let component: T; // The first index of the first selector is the tag name. @@ -157,7 +158,7 @@ export function createRootContext(scheduler: (workFn: () => void) => void): Root * renderComponent(AppComponent, {features: [RootLifecycleHooks]}); * ``` */ -export function LifecycleHooksFeature(component: any, def: ComponentDef): void { +export function LifecycleHooksFeature(component: any, def: ComponentDefInternal): void { const elementNode = _getComponentHostLElementNode(component); // Root component is always created at dir index 0 diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index a903ca309c..a3aca371ee 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -19,7 +19,7 @@ import {Type} from '../type'; import {assertComponentType, assertDefined} from './assert'; import {createRootContext} from './component'; import {baseDirectiveCreate, createLViewData, createTView, enterView, hostElement, initChangeDetectorIfExisting, locateHostElement} from './instructions'; -import {ComponentDef, ComponentType} from './interfaces/definition'; +import {ComponentDefInternal, ComponentType} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {RElement} from './interfaces/renderer'; import {INJECTOR, LViewData, LViewFlags, RootContext} from './interfaces/view'; @@ -72,7 +72,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { return toRefArray(this.componentDef.outputs); } - constructor(private componentDef: ComponentDef) { + constructor(private componentDef: ComponentDefInternal) { super(); this.componentType = componentDef.type; this.selector = componentDef.selectors[0][0] as string; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index e39abdb9b7..fd6a4b08d4 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -11,12 +11,13 @@ import {ChangeDetectionStrategy} from '../change_detection/constants'; import {PipeTransform} from '../change_detection/pipe_transform'; import {Provider} from '../core'; import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks'; +import {NgModuleDef, NgModuleDefInternal} from '../metadata/ng_module'; import {RendererType2} from '../render/api'; import {Type} from '../type'; import {resolveRendererType2} from '../view/util'; import {diPublic} from './di'; -import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFeature, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition'; +import {ComponentDefFeature, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFeature, DirectiveDefInternal, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition'; import {CssSelectorList, SelectorFlags} from './interfaces/projection'; @@ -166,7 +167,7 @@ export function defineComponent(componentDefinition: { const type = componentDefinition.type; const pipeTypes = componentDefinition.pipes !; const directiveTypes = componentDefinition.directives !; - const def = >{ + const def: ComponentDefInternal = { type: type, diPublic: null, factory: componentDefinition.factory, @@ -176,7 +177,7 @@ export function defineComponent(componentDefinition: { inputs: invertObject(componentDefinition.inputs), outputs: invertObject(componentDefinition.outputs), rendererType: resolveRendererType2(componentDefinition.rendererType) || null, - exportAs: componentDefinition.exportAs, + exportAs: componentDefinition.exportAs || null, onInit: type.prototype.ngOnInit || null, doCheck: type.prototype.ngDoCheck || null, afterContentInit: type.prototype.ngAfterContentInit || null, @@ -200,7 +201,7 @@ export function defineComponent(componentDefinition: { } export function extractDirectiveDef(type: DirectiveType& ComponentType): - DirectiveDef|ComponentDef { + DirectiveDefInternal|ComponentDefInternal { const def = type.ngComponentDef || type.ngDirectiveDef; if (ngDevMode && !def) { throw new Error(`'${type.name}' is neither 'ComponentType' or 'DirectiveType'.`); @@ -216,7 +217,17 @@ export function extractPipeDef(type: PipeType): PipeDef { return def; } - +export function defineNgModule(def: {type: T} & Partial>): never { + const res: NgModuleDefInternal = { + type: def.type, + bootstrap: def.bootstrap || [], + declarations: def.declarations || [], + imports: def.imports || [], + exports: def.exports || [], + transitiveCompileScopes: null, + }; + return res as never; +} const PRIVATE_PREFIX = '__ngOnChanges_'; @@ -250,7 +261,7 @@ type OnChangesExpando = OnChanges & { */ export function NgOnChangesFeature(inputPropertyNames?: {[key: string]: string}): DirectiveDefFeature { - return function(definition: DirectiveDef): void { + return function(definition: DirectiveDefInternal): void { const inputs = definition.inputs; const proto = definition.type.prototype; for (let pubKey in inputs) { @@ -313,7 +324,7 @@ export function NgOnChangesFeature(inputPropertyNames?: {[key: string]: string}) } -export function PublicFeature(definition: DirectiveDef) { +export function PublicFeature(definition: DirectiveDefInternal) { definition.diPublic = diPublic; } diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index b5b9ad78d3..24860481ea 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -21,7 +21,7 @@ import {Type} from '../type'; import {assertDefined, assertGreaterThan, assertLessThan} from './assert'; import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; import {VIEWS} from './interfaces/container'; -import {ComponentTemplate, DirectiveDef} from './interfaces/definition'; +import {ComponentTemplate, DirectiveDefInternal} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node'; import {LQueries, QueryReadType} from './interfaces/query'; @@ -141,7 +141,7 @@ export function getOrCreateNodeInjectorForNode(node: LElementNode | LContainerNo * @param di The node injector in which a directive will be added * @param def The definition of the directive to be made public */ -export function diPublicInInjector(di: LInjector, def: DirectiveDef): void { +export function diPublicInInjector(di: LInjector, def: DirectiveDefInternal): void { bloomAdd(di, def.type); } @@ -150,7 +150,7 @@ export function diPublicInInjector(di: LInjector, def: DirectiveDef): void * * @param def The definition of the directive to be made public */ -export function diPublic(def: DirectiveDef): void { +export function diPublic(def: DirectiveDefInternal): void { diPublicInInjector(getOrCreateNodeInjector(), def); } @@ -376,7 +376,7 @@ export function getOrCreateInjectable( for (let i = start; i < end; i++) { // Get the definition for the directive at this index and, if it is injectable (diPublic), // and matches the given token, return the directive instance. - const directiveDef = defs[i] as DirectiveDef; + const directiveDef = defs[i] as DirectiveDefInternal; if (directiveDef.type === token && directiveDef.diPublic) { return getDirectiveInstance(node.view[DIRECTIVES] ![i]); } @@ -409,7 +409,7 @@ function searchMatchesQueuedForCreation(node: LNode, token: any): T|null { const matches = node.view[TVIEW].currentMatches; if (matches) { for (let i = 0; i < matches.length; i += 2) { - const def = matches[i] as DirectiveDef; + const def = matches[i] as DirectiveDefInternal; if (def.type === token) { return resolveDirective(def, i + 1, matches, node.view[TVIEW]); } diff --git a/packages/core/src/render3/hooks.ts b/packages/core/src/render3/hooks.ts index 5b49b24b78..f05f673b58 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -7,7 +7,7 @@ */ import {assertEqual} from './assert'; -import {DirectiveDef} from './interfaces/definition'; +import {DirectiveDefInternal} from './interfaces/definition'; import {TNodeFlags} from './interfaces/node'; import {DIRECTIVES, FLAGS, HookData, LViewData, LViewFlags, TView} from './interfaces/view'; @@ -52,7 +52,7 @@ export function queueLifecycleHooks(flags: number, tView: TView): void { // directiveCreate) so we can preserve the current hook order. Content, view, and destroy // hooks for projected components and directives must be called *before* their hosts. for (let i = start; i < end; i++) { - const def: DirectiveDef = tView.directives ![i]; + const def: DirectiveDefInternal = tView.directives ![i]; queueContentHooks(def, tView, i); queueViewHooks(def, tView, i); queueDestroyHooks(def, tView, i); @@ -61,7 +61,7 @@ export function queueLifecycleHooks(flags: number, tView: TView): void { } /** Queues afterContentInit and afterContentChecked hooks on TView */ -function queueContentHooks(def: DirectiveDef, tView: TView, i: number): void { +function queueContentHooks(def: DirectiveDefInternal, tView: TView, i: number): void { if (def.afterContentInit) { (tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentInit); } @@ -73,7 +73,7 @@ function queueContentHooks(def: DirectiveDef, tView: TView, i: number): voi } /** Queues afterViewInit and afterViewChecked hooks on TView */ -function queueViewHooks(def: DirectiveDef, tView: TView, i: number): void { +function queueViewHooks(def: DirectiveDefInternal, tView: TView, i: number): void { if (def.afterViewInit) { (tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewInit); } @@ -85,7 +85,7 @@ function queueViewHooks(def: DirectiveDef, tView: TView, i: number): void { } /** Queues onDestroy hooks on TView */ -function queueDestroyHooks(def: DirectiveDef, tView: TView, i: number): void { +function queueDestroyHooks(def: DirectiveDefInternal, tView: TView, i: number): void { if (def.onDestroy != null) { (tView.destroyHooks || (tView.destroyHooks = [])).push(i, def.onDestroy); } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index df2efee76c..7ff3835145 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -7,8 +7,8 @@ */ import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; -import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition'; -import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType, PipeDef} from './interfaces/definition'; +import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; +import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition'; export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref'; export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; @@ -16,6 +16,7 @@ export {RenderFlags} from './interfaces/definition'; export {CssSelectorList} from './interfaces/projection'; + // Naming scheme: // - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View), // C(Container), L(Listener) @@ -116,10 +117,10 @@ export { // clang-format on export { - ComponentDef, + ComponentDefInternal, ComponentTemplate, ComponentType, - DirectiveDef, + DirectiveDefInternal, DirectiveDefFlags, DirectiveType, NgOnChangesFeature, @@ -128,6 +129,7 @@ export { LifecycleHooksFeature, defineComponent, defineDirective, + defineNgModule, definePipe, getHostElement, getRenderedText, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 10ea38172c..774ecc245d 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -23,7 +23,7 @@ import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNode import {assertNodeType} from './node_assert'; import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; +import {ComponentDefInternal, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {isDifferent, stringify} from './util'; import {ViewRef} from './view_ref'; @@ -258,7 +258,7 @@ export function setHostBindings(bindings: number[] | null): void { const defs = tView.directives !; for (let i = 0; i < bindings.length; i += 2) { const dirIndex = bindings[i]; - const def = defs[dirIndex] as DirectiveDef; + const def = defs[dirIndex] as DirectiveDefInternal; def.hostBindings && def.hostBindings(dirIndex, bindings[i + 1]); } } @@ -659,7 +659,7 @@ function cacheMatchingDirectivesForNode( const matches = tView.currentMatches = findDirectiveMatches(tNode); if (matches) { for (let i = 0; i < matches.length; i += 2) { - const def = matches[i] as DirectiveDef; + const def = matches[i] as DirectiveDefInternal; const valueIndex = i + 1; resolveDirective(def, valueIndex, matches, tView); saveNameToExportMap(matches[valueIndex] as number, def, exportsMap); @@ -676,7 +676,7 @@ function findDirectiveMatches(tNode: TNode): CurrentMatchesList|null { for (let i = 0; i < registry.length; i++) { const def = registry[i]; if (isNodeMatchingSelectorList(tNode, def.selectors !)) { - if ((def as ComponentDef).template) { + if ((def as ComponentDefInternal).template) { if (tNode.flags & TNodeFlags.isComponent) throwMultipleComponentError(tNode); tNode.flags = TNodeFlags.isComponent; } @@ -689,7 +689,8 @@ function findDirectiveMatches(tNode: TNode): CurrentMatchesList|null { } export function resolveDirective( - def: DirectiveDef, valueIndex: number, matches: CurrentMatchesList, tView: TView): any { + def: DirectiveDefInternal, valueIndex: number, matches: CurrentMatchesList, + tView: TView): any { if (matches[valueIndex] === null) { matches[valueIndex] = CIRCULAR; const instance = def.factory(); @@ -745,7 +746,7 @@ function instantiateDirectivesDirectly() { const tDirectives = tView.directives !; for (let i = start; i < end; i++) { - const def: DirectiveDef = tDirectives[i]; + const def: DirectiveDefInternal = tDirectives[i]; directiveCreate(i, def.factory(), def); } } @@ -773,11 +774,11 @@ function cacheMatchingLocalNames( * to their directive instances. */ function saveNameToExportMap( - index: number, def: DirectiveDef| ComponentDef, + index: number, def: DirectiveDefInternal| ComponentDefInternal, exportsMap: {[key: string]: number} | null) { if (exportsMap) { if (def.exportAs) exportsMap[def.exportAs] = index; - if ((def as ComponentDef).template) exportsMap[''] = index; + if ((def as ComponentDefInternal).template) exportsMap[''] = index; } } @@ -929,7 +930,7 @@ export function locateHostElement( * @returns LElementNode created */ export function hostElement( - tag: string, rNode: RElement | null, def: ComponentDef, + tag: string, rNode: RElement | null, def: ComponentDefInternal, sanitizer?: Sanitizer | null): LElementNode { resetApplicationState(); const node = createLNode( @@ -1187,7 +1188,7 @@ function generatePropertyAliases( const defs = tView.directives !; for (let i = start; i < end; i++) { - const directiveDef = defs[i] as DirectiveDef; + const directiveDef = defs[i] as DirectiveDefInternal; const propertyAliasMap: {[publicName: string]: string} = isInput ? directiveDef.inputs : directiveDef.outputs; for (let publicName in propertyAliasMap) { @@ -1387,15 +1388,16 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { * @param directiveDef DirectiveDef object which contains information about the template. */ export function directiveCreate( - index: number, directive: T, directiveDef: DirectiveDef| ComponentDef): T { + index: number, directive: T, + directiveDef: DirectiveDefInternal| ComponentDefInternal): T { const instance = baseDirectiveCreate(index, directive, directiveDef); ngDevMode && assertDefined(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); const tNode = previousOrParentNode.tNode; - const isComponent = (directiveDef as ComponentDef).template; + const isComponent = (directiveDef as ComponentDefInternal).template; if (isComponent) { - addComponentLogic(index, directive, directiveDef as ComponentDef); + addComponentLogic(index, directive, directiveDef as ComponentDefInternal); } if (firstTemplatePass) { @@ -1413,7 +1415,8 @@ export function directiveCreate( return instance; } -function addComponentLogic(directiveIndex: number, instance: T, def: ComponentDef): void { +function addComponentLogic( + directiveIndex: number, instance: T, def: ComponentDefInternal): void { const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs); // Only component views should be added to the view tree directly. Embedded views are @@ -1442,7 +1445,8 @@ function addComponentLogic(directiveIndex: number, instance: T, def: Componen * current Angular. Example: local refs and inputs on root component. */ export function baseDirectiveCreate( - index: number, directive: T, directiveDef: DirectiveDef| ComponentDef): T { + index: number, directive: T, + directiveDef: DirectiveDefInternal| ComponentDefInternal): T { ngDevMode && assertEqual(viewData[BINDING_INDEX], -1, 'directives should be created before any bindings'); ngDevMode && assertPreviousIsParent(); diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index dc41b96f33..b13d1924d0 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -54,6 +54,12 @@ export const enum DirectiveDefFlags {ContentQuery = 0b10} */ export interface PipeType extends Type { ngPipeDef: never; } +/** + * A version of {@link DirectiveDef} that represents the runtime type shape only, and excludes + * metadata parameters. + */ +export type DirectiveDefInternal = DirectiveDef; + /** * Runtime link information for Directives. * @@ -64,14 +70,16 @@ export interface PipeType extends Type { ngPipeDef: never; } * never create the object directly since the shape of this object * can change between versions. * + * @param Selector type metadata specifying the selector of the directive or component + * * See: {@link defineDirective} */ -export interface DirectiveDef { +export interface DirectiveDef { /** Token representing the directive. Used by DI. */ type: Type; /** Function that makes a directive public to the DI system. */ - diPublic: ((def: DirectiveDef) => void)|null; + diPublic: ((def: DirectiveDef) => void)|null; /** The selectors that will be used to match nodes to this directive. */ selectors: CssSelectorList; @@ -124,6 +132,12 @@ export interface DirectiveDef { onDestroy: (() => void)|null; } +/** + * A version of {@link ComponentDef} that represents the runtime type shape only, and excludes + * metadata parameters. + */ +export type ComponentDefInternal = ComponentDef; + /** * Runtime link information for Components. * @@ -136,7 +150,7 @@ export interface DirectiveDef { * * See: {@link defineComponent} */ -export interface ComponentDef extends DirectiveDef { +export interface ComponentDef extends DirectiveDef { /** * The View template of the component. * @@ -220,8 +234,8 @@ export interface PipeDef { onDestroy: (() => void)|null; } -export type DirectiveDefFeature = (directiveDef: DirectiveDef) => void; -export type ComponentDefFeature = (componentDef: ComponentDef) => void; +export type DirectiveDefFeature = (directiveDef: DirectiveDef) => void; +export type ComponentDefFeature = (componentDef: ComponentDef) => void; /** * Type used for directiveDefs on component definition. @@ -230,12 +244,12 @@ export type ComponentDefFeature = (componentDef: ComponentDef) => void; */ export type DirectiveDefListOrFactory = (() => DirectiveDefList) | DirectiveDefList; -export type DirectiveDefList = (DirectiveDef| ComponentDef)[]; +export type DirectiveDefList = (DirectiveDef| ComponentDef)[]; export type DirectiveTypesOrFactory = (() => DirectiveTypeList) | DirectiveTypeList; export type DirectiveTypeList = - (DirectiveDef| ComponentDef| + (DirectiveDef| ComponentDef| Type/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[]; /** diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index cbedb714e8..5f199104cc 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -10,7 +10,7 @@ import {Injector} from '../../di/injector'; import {Sanitizer} from '../../sanitization/security'; import {LContainer} from './container'; -import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDef, PipeDefList} from './definition'; +import {ComponentTemplate, DirectiveDefInternal, DirectiveDefList, PipeDef, PipeDefList} from './definition'; import {LElementNode, LViewNode, TNode} from './node'; import {LQueries} from './query'; import {Renderer3} from './renderer'; @@ -443,7 +443,7 @@ export type HookData = (number | (() => void))[]; export type TData = (TNode | PipeDef| null)[]; /** Type for TView.currentMatches */ -export type CurrentMatchesList = [DirectiveDef, (string | number | null)]; +export type CurrentMatchesList = [DirectiveDefInternal, (string | number | null)]; // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 9c0b2d3fb1..b05831dae3 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -47,7 +47,9 @@ export function compileComponent(type: Type, metadata: Component): Promise< const constantPool = new ConstantPool(); // Parse the template and check for errors. - const template = parseTemplate(templateStr, `ng://${type.name}/template.html`); + const template = parseTemplate(templateStr, `ng://${type.name}/template.html`, { + preserveWhitespaces: metadata.preserveWhitespaces || false, + }); if (template.errors !== undefined) { const errors = template.errors.map(err => err.toString()).join(', '); throw new Error(`Errors during JIT compilation of template for ${type.name}: ${errors}`); diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index b4e0b8f0b5..b82abaa389 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -8,7 +8,6 @@ import {defineInjectable, defineInjector,} from '../../di/defs'; import {inject} from '../../di/injector'; -import {defineNgModule} from '../../metadata/ng_module'; import * as r3 from '../index'; @@ -22,7 +21,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵdefineDirective': r3.defineDirective, 'defineInjectable': defineInjectable, 'defineInjector': defineInjector, - 'ɵdefineNgModule': defineNgModule, + 'ɵdefineNgModule': r3.defineNgModule, 'ɵdefinePipe': r3.definePipe, 'ɵdirectiveInject': r3.directiveInject, 'inject': inject, diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index e457f3e7ca..004cf80e06 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -8,9 +8,9 @@ import {Expression, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule as compileR3NgModule, jitExpression} from '@angular/compiler'; -import {ModuleWithProviders, NgModule, NgModuleDef, NgModuleTransitiveScopes} from '../../metadata/ng_module'; +import {ModuleWithProviders, NgModule, NgModuleDefInternal, NgModuleTransitiveScopes} from '../../metadata/ng_module'; import {Type} from '../../type'; -import {ComponentDef} from '../interfaces/definition'; +import {ComponentDefInternal} from '../interfaces/definition'; import {angularCoreEnv} from './environment'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; @@ -48,7 +48,8 @@ export function compileNgModule(type: Type, ngModule: NgModule): void { if (declaration.hasOwnProperty(NG_COMPONENT_DEF)) { // An `ngComponentDef` field exists - go ahead and patch the component directly. patchComponentDefWithScope( - (declaration as Type& {ngComponentDef: ComponentDef}).ngComponentDef, type); + (declaration as Type& {ngComponentDef: ComponentDefInternal}).ngComponentDef, + type); } else if ( !declaration.hasOwnProperty(NG_DIRECTIVE_DEF) && !declaration.hasOwnProperty(NG_PIPE_DEF)) { // Set `ngSelectorScope` for future reference when the component compilation finishes. @@ -61,7 +62,8 @@ export function compileNgModule(type: Type, ngModule: NgModule): void { * Patch the definition of a component with directives and pipes from the compilation scope of * a given module. */ -export function patchComponentDefWithScope(componentDef: ComponentDef, module: Type) { +export function patchComponentDefWithScope( + componentDef: ComponentDefInternal, module: Type) { componentDef.directiveDefs = () => Array.from(transitiveScopesFor(module).compilation.directives) .map(dir => dir.ngDirectiveDef || dir.ngComponentDef) .filter(def => !!def); @@ -113,7 +115,7 @@ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveS def.imports.forEach((imported: Type) => { let importedTyped = imported as Type& { // If imported is an @NgModule: - ngModuleDef?: NgModuleDef; + ngModuleDef?: NgModuleDefInternal; }; if (!isNgModule(importedTyped)) { @@ -132,7 +134,7 @@ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveS // Components, Directives, NgModules, and Pipes can all be exported. ngComponentDef?: any; ngDirectiveDef?: any; - ngModuleDef?: NgModuleDef; + ngModuleDef?: NgModuleDefInternal; ngPipeDef?: any; }; @@ -188,6 +190,6 @@ function isModuleWithProviders(value: any): value is ModuleWithProviders { return (value as{ngModule?: any}).ngModule !== undefined; } -function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDef} { - return (value as{ngModuleDef?: NgModuleDef}).ngModuleDef !== undefined; +function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDefInternal} { + return (value as{ngModuleDef?: NgModuleDefInternal}).ngModuleDef !== undefined; } diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 241df49d51..1ec3ed22ee 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -18,7 +18,7 @@ import {getSymbolIterator} from '../util'; import {assertDefined, assertEqual} from './assert'; import {ReadFromInjectorFn, getOrCreateNodeInjectorForNode} from './di'; import {assertPreviousIsParent, getCurrentQueries, store, storeCleanupWithContext} from './instructions'; -import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition'; +import {DirectiveDefInternal, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition'; import {LInjector, unusedValueExportToPlacateAjd as unused2} from './interfaces/injector'; import {LContainerNode, LElementNode, LNode, TNode, TNodeFlags, unusedValueExportToPlacateAjd as unused3} from './interfaces/node'; import {LQueries, QueryReadType, unusedValueExportToPlacateAjd as unused4} from './interfaces/query'; @@ -232,7 +232,7 @@ function getIdxOfMatchingDirective(node: LNode, type: Type): number|null { const start = flags >> TNodeFlags.DirectiveStartingIndexShift; const end = start + count; for (let i = start; i < end; i++) { - const def = defs[i] as DirectiveDef; + const def = defs[i] as DirectiveDefInternal; if (def.type === type && def.diPublic) { return i; } diff --git a/packages/core/test/bundling/hello_world/BUILD.bazel b/packages/core/test/bundling/hello_world/BUILD.bazel index ef122d93fa..eebdb88453 100644 --- a/packages/core/test/bundling/hello_world/BUILD.bazel +++ b/packages/core/test/bundling/hello_world/BUILD.bazel @@ -49,6 +49,7 @@ jasmine_node_test( ], tags = [ "ivy-jit", + "ivy-local", ], deps = [":test_lib"], ) diff --git a/packages/core/test/bundling/todo/BUILD.bazel b/packages/core/test/bundling/todo/BUILD.bazel index ca386bde01..450ccb2b45 100644 --- a/packages/core/test/bundling/todo/BUILD.bazel +++ b/packages/core/test/bundling/todo/BUILD.bazel @@ -51,7 +51,11 @@ jasmine_node_test( ":bundle.min.js", ":bundle.min_debug.js", ], - tags = ["ivy-jit"], + tags = [ + "ivy-jit", + "ivy-local", + "ivy-only", + ], deps = [":test_lib"], ) diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 7e32c98327..80369b3b06 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -26,24 +26,12 @@ { "name": "DIRECTIVES" }, - { - "name": "DefaultIterableDiffer" - }, - { - "name": "DefaultIterableDifferFactory" - }, { "name": "EMPTY$2" }, { "name": "EMPTY_RENDERER_TYPE_ID" }, - { - "name": "ElementRef$1" - }, - { - "name": "EmbeddedViewRef$1" - }, { "name": "FLAGS" }, @@ -59,12 +47,6 @@ { "name": "INJECTOR$1" }, - { - "name": "IterableChangeRecord_" - }, - { - "name": "IterableDiffers" - }, { "name": "NEXT" }, @@ -92,12 +74,6 @@ { "name": "NgIfContext" }, - { - "name": "Optional" - }, - { - "name": "PARAMETERS" - }, { "name": "PARENT" }, @@ -107,9 +83,6 @@ { "name": "RENDERER" }, - { - "name": "RENDER_PARENT" - }, { "name": "ROOT_DIRECTIVE_INDICES" }, @@ -119,18 +92,12 @@ { "name": "SANITIZER" }, - { - "name": "SkipSelf" - }, { "name": "TAIL" }, { "name": "TVIEW" }, - { - "name": "TemplateRef$1" - }, { "name": "ToDoAppComponent" }, @@ -149,36 +116,12 @@ { "name": "VIEWS" }, - { - "name": "ViewContainerRef$1" - }, { "name": "_CLEAN_PROMISE" }, - { - "name": "_DuplicateItemRecordList" - }, - { - "name": "_DuplicateMap" - }, { "name": "_ROOT_DIRECTIVE_INDICES" }, - { - "name": "__extends" - }, - { - "name": "__read" - }, - { - "name": "__self" - }, - { - "name": "__spread" - }, - { - "name": "__window" - }, { "name": "_c0" }, @@ -242,21 +185,12 @@ { "name": "_getComponentHostLElementNode" }, - { - "name": "_global" - }, { "name": "_renderCompCount" }, - { - "name": "_symbolIterator" - }, { "name": "addComponentLogic" }, - { - "name": "addRemoveViewFromContainer" - }, { "name": "addToViewTree" }, @@ -293,15 +227,9 @@ { "name": "canInsertNativeNode" }, - { - "name": "checkNoChanges" - }, { "name": "checkNoChangesMode" }, - { - "name": "cleanUpView" - }, { "name": "componentRefresh" }, @@ -341,27 +269,12 @@ { "name": "defineComponent" }, - { - "name": "defineDirective" - }, { "name": "defineInjectable" }, { "name": "defineInjector" }, - { - "name": "destroyLView" - }, - { - "name": "destroyViewTree" - }, - { - "name": "detachView" - }, - { - "name": "detectChanges" - }, { "name": "detectChangesInternal" }, @@ -398,18 +311,6 @@ { "name": "executeInitHooks" }, - { - "name": "executeNodeAction" - }, - { - "name": "executeOnDestroys" - }, - { - "name": "executePipeOnDestroys" - }, - { - "name": "extendStatics" - }, { "name": "extractDirectiveDef" }, @@ -431,9 +332,6 @@ { "name": "generatePropertyAliases" }, - { - "name": "getChildLNode" - }, { "name": "getCleanup" }, @@ -446,18 +344,6 @@ { "name": "getLViewChild" }, - { - "name": "getNextLNode" - }, - { - "name": "getNextLNodeWithProjection" - }, - { - "name": "getOrCreateContainerRef" - }, - { - "name": "getOrCreateElementRef" - }, { "name": "getOrCreateInjectable" }, @@ -470,39 +356,21 @@ { "name": "getOrCreateTView" }, - { - "name": "getOrCreateTemplateRef" - }, { "name": "getParentLNode" }, - { - "name": "getParentState" - }, - { - "name": "getPreviousIndex" - }, { "name": "getPreviousOrParentNode" }, { "name": "getRenderFlags" }, - { - "name": "getRenderer" - }, { "name": "getRootView" }, - { - "name": "getSymbolIterator" - }, { "name": "getTViewCleanup" }, - { - "name": "getTypeNameForDebugging" - }, { "name": "getTypeNameForDebugging$1" }, @@ -518,15 +386,6 @@ { "name": "inject" }, - { - "name": "injectTemplateRef" - }, - { - "name": "injectViewContainerRef" - }, - { - "name": "insertView" - }, { "name": "instantiateDirectivesDirectly" }, @@ -545,12 +404,6 @@ { "name": "isDifferent" }, - { - "name": "isJsObject" - }, - { - "name": "isListLikeIterable" - }, { "name": "isNodeMatchingSelector" }, @@ -563,9 +416,6 @@ { "name": "isProceduralRenderer" }, - { - "name": "iterateListLike" - }, { "name": "leaveView" }, @@ -581,15 +431,6 @@ { "name": "locateHostElement" }, - { - "name": "looseIdentical" - }, - { - "name": "makeMetadataCtor" - }, - { - "name": "makeParamDecorator" - }, { "name": "markDirtyIfOnPush" }, @@ -599,9 +440,6 @@ { "name": "namespaceHTML" }, - { - "name": "notImplemented" - }, { "name": "queueComponentIndexForCheck" }, @@ -632,12 +470,6 @@ { "name": "refreshView" }, - { - "name": "removeListeners" - }, - { - "name": "removeView" - }, { "name": "renderComponent" }, @@ -716,15 +548,9 @@ { "name": "tickRootContext" }, - { - "name": "trackByIdentity" - }, { "name": "viewAttached" }, - { - "name": "walkLNodeTree" - }, { "name": "wrapListenerWithDirtyAndDefault" }, diff --git a/packages/core/test/bundling/todo/index.ts b/packages/core/test/bundling/todo/index.ts index 579335ce45..a2acf790be 100644 --- a/packages/core/test/bundling/todo/index.ts +++ b/packages/core/test/bundling/todo/index.ts @@ -147,36 +147,7 @@ export class ToDoAppComponent { } } -// In JIT mode the @Directive decorators in //packages/common will compile the Ivy fields. When -// running under --define=compile=legacy, //packages/common is not compiled with Ivy fields, so they -// must be monkey-patched on. -if (!(NgIf as any).ngDirectiveDef) { - // TODO(misko): This hack is here because common is not compiled with Ivy flag turned on. - (CommonModule as any).ngInjectorDef = defineInjector({factory: () => new CommonModule}); - - // TODO(misko): This hack is here because common is not compiled with Ivy flag turned on. - (NgForOf as any).ngDirectiveDef = defineDirective({ - type: NgForOf, - selectors: [['', 'ngFor', '', 'ngForOf', '']], - factory: () => new NgForOf( - injectViewContainerRef(), injectTemplateRef(), directiveInject(IterableDiffers)), - inputs: { - ngForOf: 'ngForOf', - ngForTrackBy: 'ngForTrackBy', - ngForTemplate: 'ngForTemplate', - } - }); - - // TODO(misko): This hack is here because common is not compiled with Ivy flag turned on. - (NgIf as any).ngDirectiveDef = defineDirective({ - type: NgIf, - selectors: [['', 'ngIf', '']], - factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef()), - inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'} - }); -} - -@NgModule({declarations: [ToDoAppComponent, ToDoAppComponent], imports: [CommonModule]}) +@NgModule({declarations: [ToDoAppComponent], imports: [CommonModule]}) export class ToDoAppModule { } diff --git a/packages/core/test/render3/compiler_canonical/component_directives_spec.ts b/packages/core/test/render3/compiler_canonical/component_directives_spec.ts index 9f2bb7c85c..90cd8ce01c 100644 --- a/packages/core/test/render3/compiler_canonical/component_directives_spec.ts +++ b/packages/core/test/render3/compiler_canonical/component_directives_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {renderComponent, toHtml} from '../render_util'; @@ -78,8 +78,9 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyComponent.ngComponentDef as ComponentDef).directiveDefs = - [(ChildComponent.ngComponentDef as ComponentDef), SomeDirective.ngDirectiveDef]; + (MyComponent.ngComponentDef as ComponentDefInternal).directiveDefs = [ + (ChildComponent.ngComponentDef as ComponentDefInternal), SomeDirective.ngDirectiveDef + ]; // /NON-NORMATIVE expect(renderComp(MyComponent)).toEqual('child-view!'); @@ -127,7 +128,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = [HostBindingDir.ngDirectiveDef]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [HostBindingDir.ngDirectiveDef]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`
`); @@ -178,7 +180,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = [HostListenerDir.ngDirectiveDef]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [HostListenerDir.ngDirectiveDef]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(``); @@ -222,7 +225,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = [HostAttributeDir.ngDirectiveDef]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [HostAttributeDir.ngDirectiveDef]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`
`); @@ -269,7 +273,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = [HostBindingDir.ngDirectiveDef]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [HostBindingDir.ngDirectiveDef]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`
`); @@ -331,8 +336,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`some name`); @@ -461,8 +466,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyArrayComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyArrayComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`Nancy Bess`); @@ -506,8 +511,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyArrayComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyArrayComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`NANCY Bess`); @@ -571,8 +576,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`3`); @@ -614,8 +619,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyArrayComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyArrayComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`Nancy Bess`); @@ -727,8 +732,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(MyComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(MyComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`start-abcde-middle-fghi-end`); @@ -802,8 +807,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(ObjectComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(ObjectComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)).toEqual(`

500

slide

`); @@ -890,8 +895,8 @@ describe('components & directives', () => { } // NON-NORMATIVE (done by defineNgModule) - (MyApp.ngComponentDef as ComponentDef).directiveDefs = - [(NestedComp.ngComponentDef as ComponentDef)]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = + [(NestedComp.ngComponentDef as ComponentDefInternal)]; // /NON-NORMATIVE expect(renderComp(MyApp)) diff --git a/packages/core/test/render3/compiler_canonical/elements_spec.ts b/packages/core/test/render3/compiler_canonical/elements_spec.ts index e82fbf5f04..536b2a92a7 100644 --- a/packages/core/test/render3/compiler_canonical/elements_spec.ts +++ b/packages/core/test/render3/compiler_canonical/elements_spec.ts @@ -11,7 +11,7 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_ut import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; import {AttributeMarker} from '../../../src/render3'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {ComponentFixture, renderComponent, toHtml} from '../render_util'; @@ -107,7 +107,8 @@ describe('elements', () => { } // NON-NORMATIVE - (LocalRefComp.ngComponentDef as ComponentDef).directiveDefs = () => [Dir.ngDirectiveDef]; + (LocalRefComp.ngComponentDef as ComponentDefInternal).directiveDefs = + () => [Dir.ngDirectiveDef]; // /NON-NORMATIVE const fixture = new ComponentFixture(LocalRefComp); diff --git a/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts b/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts index 6577777e51..6738ee94e0 100644 --- a/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts +++ b/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {renderComponent, toHtml} from '../render_util'; @@ -83,7 +83,8 @@ describe('lifecycle hooks', () => { } // NON-NORMATIVE - (SimpleLayout.ngComponentDef as ComponentDef).directiveDefs = [LifecycleComp.ngComponentDef]; + (SimpleLayout.ngComponentDef as ComponentDefInternal).directiveDefs = + [LifecycleComp.ngComponentDef]; // /NON-NORMATIVE it('should gen hooks with a few simple components', () => { diff --git a/packages/core/test/render3/compiler_canonical/pipes_spec.ts b/packages/core/test/render3/compiler_canonical/pipes_spec.ts index ea6316fdac..90bf0ff7e0 100644 --- a/packages/core/test/render3/compiler_canonical/pipes_spec.ts +++ b/packages/core/test/render3/compiler_canonical/pipes_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {containerEl, renderComponent, toHtml} from '../render_util'; @@ -101,7 +101,7 @@ describe('pipes', () => { } // NON-NORMATIVE - (MyApp.ngComponentDef as ComponentDef).pipeDefs = + (MyApp.ngComponentDef as ComponentDefInternal).pipeDefs = () => [MyPurePipe.ngPipeDef, MyPipe.ngPipeDef]; // /NON-NORMATIVE @@ -197,8 +197,8 @@ describe('pipes', () => { } // NON-NORMATIVE - (MyApp.ngComponentDef as ComponentDef).directiveDefs = [OneTimeIf.ngDirectiveDef]; - (MyApp.ngComponentDef as ComponentDef).pipeDefs = [MyPurePipe.ngPipeDef]; + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = [OneTimeIf.ngDirectiveDef]; + (MyApp.ngComponentDef as ComponentDefInternal).pipeDefs = [MyPurePipe.ngPipeDef]; // /NON-NORMATIVE let myApp: MyApp = renderComponent(MyApp); diff --git a/packages/core/test/render3/compiler_canonical/query_spec.ts b/packages/core/test/render3/compiler_canonical/query_spec.ts index 10f6f1221e..98c3a6151d 100644 --- a/packages/core/test/render3/compiler_canonical/query_spec.ts +++ b/packages/core/test/render3/compiler_canonical/query_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {renderComponent, toHtml} from '../render_util'; @@ -71,7 +71,7 @@ describe('queries', () => { } // NON-NORMATIVE - (ViewQueryComponent.ngComponentDef as ComponentDef).directiveDefs = + (ViewQueryComponent.ngComponentDef as ComponentDefInternal).directiveDefs = [SomeDirective.ngDirectiveDef]; // /NON-NORMATIVE @@ -155,7 +155,7 @@ describe('queries', () => { } // NON-NORMATIVE - (MyApp.ngComponentDef as ComponentDef).directiveDefs = + (MyApp.ngComponentDef as ComponentDefInternal).directiveDefs = [ContentQueryComponent.ngComponentDef, SomeDirective.ngDirectiveDef]; // /NON-NORMATIVE diff --git a/packages/core/test/render3/compiler_canonical/small_app_spec.ts b/packages/core/test/render3/compiler_canonical/small_app_spec.ts index 93e795d829..afc631d7c9 100644 --- a/packages/core/test/render3/compiler_canonical/small_app_spec.ts +++ b/packages/core/test/render3/compiler_canonical/small_app_spec.ts @@ -96,7 +96,7 @@ class ToDoAppComponent { } // NON-NORMATIVE -(ToDoAppComponent.ngComponentDef as r3.ComponentDef).directiveDefs = () => +(ToDoAppComponent.ngComponentDef as r3.ComponentDefInternal).directiveDefs = () => [ToDoItemComponent.ngComponentDef, (NgForOf as r3.DirectiveType>).ngDirectiveDef]; // /NON-NORMATIVE diff --git a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts index 26c198b24c..8f57e5d805 100644 --- a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts +++ b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {ComponentDef} from '../../../src/render3/interfaces/definition'; +import {ComponentDefInternal} from '../../../src/render3/interfaces/definition'; import {renderComponent, toHtml} from '../render_util'; @@ -125,7 +125,7 @@ describe('template variables', () => { } // NON-NORMATIVE - (MyComponent.ngComponentDef as ComponentDef).directiveDefs = + (MyComponent.ngComponentDef as ComponentDefInternal).directiveDefs = [ForOfDirective.ngDirectiveDef]; // /NON-NORMATIVE diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index 148f447ded..c73fc4c0f6 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -11,7 +11,7 @@ import {DoCheck, ViewEncapsulation, createInjector, defineInjectable, defineInje import {getRenderedText} from '../../src/render3/component'; import {ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding, tick} from '../../src/render3/instructions'; -import {ComponentDef, DirectiveDef, RenderFlags} from '../../src/render3/interfaces/definition'; +import {ComponentDefInternal, DirectiveDefInternal, RenderFlags} from '../../src/render3/interfaces/definition'; import {createRendererType2} from '../../src/view/index'; import {getRendererFactory2} from './imported_renderer2'; @@ -346,7 +346,7 @@ describe('recursive components', () => { }); } - (TreeComponent.ngComponentDef as ComponentDef).directiveDefs = + (TreeComponent.ngComponentDef as ComponentDefInternal).directiveDefs = () => [TreeComponent.ngComponentDef]; function _buildTree(currDepth: number): TreeNode { diff --git a/packages/core/test/render3/define_spec.ts b/packages/core/test/render3/define_spec.ts index 6c9f6565e7..165895866d 100644 --- a/packages/core/test/render3/define_spec.ts +++ b/packages/core/test/render3/define_spec.ts @@ -7,7 +7,7 @@ */ import {DoCheck, OnChanges, SimpleChange, SimpleChanges} from '../../src/core'; -import {DirectiveDef, NgOnChangesFeature, defineDirective} from '../../src/render3/index'; +import {DirectiveDefInternal, NgOnChangesFeature, defineDirective} from '../../src/render3/index'; describe('define', () => { describe('component', () => { @@ -36,15 +36,15 @@ describe('define', () => { }); } - const myDir = - (MyDirective.ngDirectiveDef as DirectiveDef).factory() as MyDirective; + const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as MyDirective; myDir.valA = 'first'; expect(myDir.valA).toEqual('first'); myDir.valB = 'second'; expect(myDir.log).toEqual(['second']); expect(myDir.valB).toEqual('works'); myDir.log.length = 0; - (MyDirective.ngDirectiveDef as DirectiveDef).doCheck !.call(myDir); + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); const changeA = new SimpleChange(undefined, 'first', true); const changeB = new SimpleChange(undefined, 'second', true); expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']); @@ -70,18 +70,18 @@ describe('define', () => { }); } - const myDir = - (MyDirective.ngDirectiveDef as DirectiveDef).factory() as MyDirective; + const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as MyDirective; myDir.valA = 'first'; myDir.valB = 'second'; - (MyDirective.ngDirectiveDef as DirectiveDef).doCheck !.call(myDir); + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); const changeA1 = new SimpleChange(undefined, 'first', true); const changeB1 = new SimpleChange(undefined, 'second', true); expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]); myDir.log.length = 0; myDir.valA = 'third'; - (MyDirective.ngDirectiveDef as DirectiveDef).doCheck !.call(myDir); + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); const changeA2 = new SimpleChange('first', 'third', false); expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]); }); @@ -106,11 +106,11 @@ describe('define', () => { }); } - const myDir = - (MyDirective.ngDirectiveDef as DirectiveDef).factory() as MyDirective; + const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as MyDirective; myDir.onlySetter = 'someValue'; expect(myDir.onlySetter).toBeUndefined(); - (MyDirective.ngDirectiveDef as DirectiveDef).doCheck !.call(myDir); + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); const changeSetter = new SimpleChange(undefined, 'someValue', true); expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]); }); diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index 1a82a60372..6d31afb2bf 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -10,8 +10,8 @@ import {Injectable} from '@angular/core/src/di/injectable'; import {inject, setCurrentInjector} from '@angular/core/src/di/injector'; import {ivyEnabled} from '@angular/core/src/ivy_switch'; import {Component, HostBinding, HostListener} from '@angular/core/src/metadata/directives'; -import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module'; -import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; +import {NgModule, NgModuleDefInternal} from '@angular/core/src/metadata/ng_module'; +import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition'; ivyEnabled && describe('render3 jit', () => { let injector: any; @@ -134,7 +134,7 @@ ivyEnabled && describe('render3 jit', () => { class Module { } - const moduleDef: NgModuleDef = (Module as any).ngModuleDef; + const moduleDef: NgModuleDefInternal = (Module as any).ngModuleDef; expect(moduleDef).toBeDefined(); expect(moduleDef.declarations.length).toBe(1); expect(moduleDef.declarations[0]).toBe(Cmp); @@ -147,7 +147,7 @@ ivyEnabled && describe('render3 jit', () => { }) class Cmp { } - const cmpDef: ComponentDef = (Cmp as any).ngComponentDef; + const cmpDef: ComponentDefInternal = (Cmp as any).ngComponentDef; expect(cmpDef.directiveDefs).toBeNull(); @@ -157,7 +157,7 @@ ivyEnabled && describe('render3 jit', () => { class Module { } - const moduleDef: NgModuleDef = (Module as any).ngModuleDef; + const moduleDef: NgModuleDefInternal = (Module as any).ngModuleDef; expect(cmpDef.directiveDefs instanceof Function).toBe(true); expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]); }); @@ -179,7 +179,7 @@ ivyEnabled && describe('render3 jit', () => { onChange(event: any): void {} } - const cmpDef = (Cmp as any).ngComponentDef as ComponentDef; + const cmpDef = (Cmp as any).ngComponentDef as ComponentDefInternal; expect(cmpDef.hostBindings).toBeDefined(); expect(cmpDef.hostBindings !.length).toBe(2); diff --git a/packages/core/test/render3/jit_environment_spec.ts b/packages/core/test/render3/jit_environment_spec.ts index c451f856a0..b88abe1a84 100644 --- a/packages/core/test/render3/jit_environment_spec.ts +++ b/packages/core/test/render3/jit_environment_spec.ts @@ -14,6 +14,7 @@ import {angularCoreEnv} from '../../src/render3/jit/environment'; const INTERFACE_EXCEPTIONS = new Set([ 'ComponentDef', 'DirectiveDef', + 'NgModuleDef', ]); describe('r3 jit environment', () => { diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 5d08734f11..666f8dba99 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -11,7 +11,7 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_ut import {Injector} from '../../src/di/injector'; import {CreateComponentOptions} from '../../src/render3/component'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; -import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; +import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions'; import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {LElementNode} from '../../src/render3/interfaces/node'; @@ -180,13 +180,13 @@ export function renderToHtml( function toDefs( types: DirectiveTypesOrFactory | undefined | null, - mapFn: (type: Type) => DirectiveDef): DirectiveDefList|null; + mapFn: (type: Type) => DirectiveDefInternal): DirectiveDefList|null; function toDefs( types: PipeTypesOrFactory | undefined | null, mapFn: (type: Type) => PipeDef): PipeDefList|null; function toDefs( types: PipeTypesOrFactory | DirectiveTypesOrFactory | undefined | null, - mapFn: (type: Type) => PipeDef| DirectiveDef): any { + mapFn: (type: Type) => PipeDef| DirectiveDefInternal): any { if (!types) return null; if (typeof types == 'function') { types = types(); diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 7f0176ab52..79a0a5e0dc 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -389,6 +389,7 @@ export class TestBed implements Injector { providers: [ ...rootProviderOverrides, ], + jit: true, }) class RootScopeModule { } @@ -399,7 +400,7 @@ export class TestBed implements Injector { const imports = [rootScopeImports, this.ngModule, this._imports]; const schemas = this._schemas; - @NgModule({providers, declarations, imports, schemas}) + @NgModule({providers, declarations, imports, schemas, jit: true}) class DynamicTestModule { } @@ -540,7 +541,7 @@ export class TestBed implements Injector { overrideTemplateUsingTestingModule(component: Type, template: string) { this._assertNotInstantiated('overrideTemplateUsingTestingModule', 'override template'); - @Component({selector: 'empty', template}) + @Component({selector: 'empty', template, jit: true}) class OverrideComponent { }