From 10a7c876929683234c6331905301727b1b337674 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 21 Mar 2021 17:31:20 +0000 Subject: [PATCH] refactor(compiler): implement `ngDeclareInjectable()` (#41316) This commit changes the partial compilation so that it outputs declarations rather than definitions for injectables. The JIT compiler and the linker are updated to be able to handle these new declarations. PR Close #41316 --- .../partial_component_linker_1.ts | 47 +----- .../partial_factory_linker_1.ts | 32 +--- .../partial_injectable_linker_1.ts | 69 ++++++++ .../partial_linker_selector.ts | 11 +- .../src/file_linker/partial_linkers/util.ts | 68 +++++++- .../partial_linker_selector_spec.ts | 4 + .../src/ngtsc/annotations/src/directive.ts | 4 +- .../src/ngtsc/annotations/src/factory.ts | 2 + .../src/ngtsc/annotations/src/injectable.ts | 130 ++++++--------- .../src/ngtsc/annotations/src/util.ts | 24 +-- .../ng_modules/GOLDEN_PARTIAL.js | 8 +- .../r3_view_compiler_di/di/GOLDEN_PARTIAL.js | 87 +++++++--- .../r3_view_compiler_di/di/TEST_CASES.json | 14 ++ .../di/providedin_forwardref.js | 8 + .../di/providedin_forwardref.ts | 12 ++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 6 +- packages/compiler/src/compile_metadata.ts | 5 + packages/compiler/src/compiler.ts | 1 + .../compiler/src/compiler_facade_interface.ts | 68 ++++---- packages/compiler/src/identifiers.ts | 3 - packages/compiler/src/injectable_compiler.ts | 11 +- .../compiler/src/injectable_compiler_2.ts | 153 ++++++++++++----- packages/compiler/src/jit_compiler_facade.ts | 81 ++++++--- packages/compiler/src/render3/partial/api.ts | 47 ++++++ .../compiler/src/render3/partial/component.ts | 6 +- .../compiler/src/render3/partial/factory.ts | 37 +---- .../src/render3/partial/injectable.ts | 93 +++++++++++ .../compiler/src/render3/partial/injector.ts | 3 + .../compiler/src/render3/partial/ng_module.ts | 3 + packages/compiler/src/render3/partial/pipe.ts | 3 +- packages/compiler/src/render3/partial/util.ts | 49 ++++++ .../compiler/src/render3/r3_identifiers.ts | 5 + .../test/compiler_facade_interface_spec.ts | 5 + .../src/compiler/compiler_facade_interface.ts | 68 ++++---- .../core/src/core_render3_private_export.ts | 1 + packages/core/src/di/jit/environment.ts | 2 + packages/core/src/di/jit/injectable.ts | 26 ++- packages/core/src/render3/jit/partial.ts | 13 +- .../core/test/acceptance/providers_spec.ts | 6 +- .../render3/jit/declare_injectable_spec.ts | 157 ++++++++++++++++++ .../core/test/render3/jit_environment_spec.ts | 2 + 41 files changed, 986 insertions(+), 388 deletions(-) create mode 100644 packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.ts create mode 100644 packages/compiler/src/render3/partial/injectable.ts create mode 100644 packages/core/test/render3/jit/declare_injectable_spec.ts diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index d2d0d09f00..f97b03c396 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -18,6 +18,7 @@ import {LinkerEnvironment} from '../linker_environment'; import {toR3DirectiveMeta} from './partial_directive_linker_1'; import {PartialLinker} from './partial_linker'; +import {extractForwardRef} from './util'; /** * A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions. @@ -81,10 +82,8 @@ export class PartialComponentLinkerVersion1 implements const type = directiveExpr.getValue('type'); const selector = directiveExpr.getString('selector'); - let typeExpr = type.getOpaque(); - const forwardRefType = extractForwardRef(type); - if (forwardRefType !== null) { - typeExpr = forwardRefType; + const {expression: typeExpr, isForwardRef} = extractForwardRef(type); + if (isForwardRef) { declarationListEmitMode = DeclarationListEmitMode.Closure; } @@ -115,13 +114,11 @@ export class PartialComponentLinkerVersion1 implements let pipes = new Map(); if (metaObj.has('pipes')) { pipes = metaObj.getObject('pipes').toMap(pipe => { - const forwardRefType = extractForwardRef(pipe); - if (forwardRefType !== null) { + const {expression: pipeType, isForwardRef} = extractForwardRef(pipe); + if (isForwardRef) { declarationListEmitMode = DeclarationListEmitMode.Closure; - return forwardRefType; - } else { - return pipe.getOpaque(); } + return pipeType; }); } @@ -277,35 +274,3 @@ function parseChangeDetectionStrategy( } return enumValue; } - -/** - * Extract the type reference expression from a `forwardRef` function call. For example, the - * expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note that this - * expression is required to be wrapped in a closure, as otherwise the forward reference would be - * resolved before initialization. - */ -function extractForwardRef(expr: AstValue): - o.WrappedNodeExpr|null { - if (!expr.isCallExpression()) { - return null; - } - - const callee = expr.getCallee(); - if (callee.getSymbolName() !== 'forwardRef') { - throw new FatalLinkerError( - callee.expression, 'Unsupported directive type, expected forwardRef or a type reference'); - } - - const args = expr.getArguments(); - if (args.length !== 1) { - throw new FatalLinkerError(expr, 'Unsupported forwardRef call, expected a single argument'); - } - - const wrapperFn = args[0] as AstValue; - if (!wrapperFn.isFunction()) { - throw new FatalLinkerError( - wrapperFn, 'Unsupported forwardRef call, expected a function argument'); - } - - return wrapperFn.getFunctionReturnValue().getOpaque(); -} diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_factory_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_factory_linker_1.ts index b8b8d375a2..a2eb3c4e52 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_factory_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_factory_linker_1.ts @@ -5,14 +5,14 @@ * 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 {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareDependencyMetadata, R3DeclareFactoryMetadata, R3DependencyMetadata, R3FactoryMetadata, R3PartialDeclaration} from '@angular/compiler'; +import {compileFactoryFunction, ConstantPool, FactoryTarget, R3DeclareFactoryMetadata, R3DependencyMetadata, R3FactoryMetadata, R3PartialDeclaration} from '@angular/compiler'; import * as o from '@angular/compiler/src/output/output_ast'; import {AstObject} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; import {PartialLinker} from './partial_linker'; -import {parseEnum, wrapReference} from './util'; +import {getDependency, parseEnum, wrapReference} from './util'; /** * A `PartialLinker` that is designed to process `ɵɵngDeclareFactory()` call expressions. @@ -45,11 +45,11 @@ export function toR3FactoryMeta( internalType: metaObj.getOpaque('type'), typeArgumentCount: 0, target: parseEnum(metaObj.getValue('target'), FactoryTarget), - deps: getDeps(metaObj, 'deps'), + deps: getDependencies(metaObj, 'deps'), }; } -function getDeps( +function getDependencies( metaObj: AstObject, propName: keyof R3DeclareFactoryMetadata): R3DependencyMetadata[]|null|'invalid' { if (!metaObj.has(propName)) { @@ -57,32 +57,10 @@ function getDeps( } const deps = metaObj.getValue(propName); if (deps.isArray()) { - return deps.getArray().map(dep => getDep(dep.getObject())); + return deps.getArray().map(dep => getDependency(dep.getObject())); } if (deps.isString()) { return 'invalid'; } return null; } - -function getDep(depObj: AstObject): - R3DependencyMetadata { - const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute'); - const token = depObj.getOpaque('token'); - // Normally `attribute` is a string literal and so its `attributeNameType` is the same string - // literal. If the `attribute` is some other expression, the `attributeNameType` would be the - // `unknown` type. It is not possible to generate this when linking, since it only deals with JS - // and not typings. When linking the existence of the `attributeNameType` only acts as a marker to - // change the injection instruction that is generated, so we just pass the literal string - // `"unknown"`. - const attributeNameType = isAttribute ? o.literal('unknown') : null; - const dep: R3DependencyMetadata = { - token, - attributeNameType, - host: depObj.has('host') && depObj.getBoolean('host'), - optional: depObj.has('optional') && depObj.getBoolean('optional'), - self: depObj.has('self') && depObj.getBoolean('self'), - skipSelf: depObj.has('skipSelf') && depObj.getBoolean('skipSelf'), - }; - return dep; -} diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts new file mode 100644 index 0000000000..33510cfe54 --- /dev/null +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {compileInjectable, ConstantPool, createR3ProviderExpression, R3DeclareInjectableMetadata, R3InjectableMetadata, R3PartialDeclaration} from '@angular/compiler'; +import * as o from '@angular/compiler/src/output/output_ast'; + +import {AstObject} from '../../ast/ast_value'; +import {FatalLinkerError} from '../../fatal_linker_error'; + +import {PartialLinker} from './partial_linker'; +import {extractForwardRef, getDependency, wrapReference} from './util'; + +/** + * A `PartialLinker` that is designed to process `ɵɵngDeclareInjectable()` call expressions. + */ +export class PartialInjectableLinkerVersion1 implements PartialLinker { + linkPartialDeclaration( + constantPool: ConstantPool, + metaObj: AstObject): o.Expression { + const meta = toR3InjectableMeta(metaObj); + const def = compileInjectable(meta, /* resolveForwardRefs */ false); + return def.expression; + } +} + +/** + * Derives the `R3InjectableMetadata` structure from the AST object. + */ +export function toR3InjectableMeta( + metaObj: AstObject): R3InjectableMetadata { + const typeExpr = metaObj.getValue('type'); + const typeName = typeExpr.getSymbolName(); + if (typeName === null) { + throw new FatalLinkerError( + typeExpr.expression, 'Unsupported type, its name could not be determined'); + } + + const meta: R3InjectableMetadata = { + name: typeName, + type: wrapReference(typeExpr.getOpaque()), + internalType: typeExpr.getOpaque(), + typeArgumentCount: 0, + providedIn: metaObj.has('providedIn') ? extractForwardRef(metaObj.getValue('providedIn')) : + createR3ProviderExpression(o.literal(null), false), + }; + + if (metaObj.has('useClass')) { + meta.useClass = extractForwardRef(metaObj.getValue('useClass')); + } + if (metaObj.has('useFactory')) { + meta.useFactory = metaObj.getOpaque('useFactory'); + } + if (metaObj.has('useExisting')) { + meta.useExisting = extractForwardRef(metaObj.getValue('useExisting')); + } + if (metaObj.has('useValue')) { + meta.useValue = extractForwardRef(metaObj.getValue('useValue')); + } + + if (metaObj.has('deps')) { + meta.deps = metaObj.getArray('deps').map(dep => getDependency(dep.getObject())); + } + + return meta; +} diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts index 3238d99bbf..771c14e8cb 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts @@ -14,6 +14,7 @@ import {LinkerEnvironment} from '../linker_environment'; import {PartialComponentLinkerVersion1} from './partial_component_linker_1'; import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1'; import {PartialFactoryLinkerVersion1} from './partial_factory_linker_1'; +import {PartialInjectableLinkerVersion1} from './partial_injectable_linker_1'; import {PartialInjectorLinkerVersion1} from './partial_injector_linker_1'; import {PartialLinker} from './partial_linker'; import {PartialNgModuleLinkerVersion1} from './partial_ng_module_linker_1'; @@ -22,12 +23,13 @@ import {PartialPipeLinkerVersion1} from './partial_pipe_linker_1'; export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective'; export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent'; export const ɵɵngDeclareFactory = 'ɵɵngDeclareFactory'; +export const ɵɵngDeclareInjectable = 'ɵɵngDeclareInjectable'; export const ɵɵngDeclareInjector = 'ɵɵngDeclareInjector'; export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule'; export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe'; export const declarationFunctions = [ - ɵɵngDeclareDirective, ɵɵngDeclareComponent, ɵɵngDeclareFactory, ɵɵngDeclareInjector, - ɵɵngDeclareNgModule, ɵɵngDeclarePipe + ɵɵngDeclareDirective, ɵɵngDeclareComponent, ɵɵngDeclareFactory, ɵɵngDeclareInjectable, + ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe ]; interface LinkerRange { @@ -93,6 +95,7 @@ export class PartialLinkerSelector { environment, createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), sourceUrl, code); const partialFactoryLinkerVersion1 = new PartialFactoryLinkerVersion1(); + const partialInjectableLinkerVersion1 = new PartialInjectableLinkerVersion1(); const partialInjectorLinkerVersion1 = new PartialInjectorLinkerVersion1(); const partialNgModuleLinkerVersion1 = new PartialNgModuleLinkerVersion1(environment.options.linkerJitMode); @@ -111,6 +114,10 @@ export class PartialLinkerSelector { {range: '0.0.0-PLACEHOLDER', linker: partialFactoryLinkerVersion1}, {range: '>=11.1.0-next.1', linker: partialFactoryLinkerVersion1}, ]); + linkers.set(ɵɵngDeclareInjectable, [ + {range: '0.0.0-PLACEHOLDER', linker: partialInjectableLinkerVersion1}, + {range: '>=11.1.0-next.1', linker: partialInjectableLinkerVersion1}, + ]); linkers.set(ɵɵngDeclareInjector, [ {range: '0.0.0-PLACEHOLDER', linker: partialInjectorLinkerVersion1}, {range: '>=11.1.0-next.1', linker: partialInjectorLinkerVersion1}, diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts index 7c8bb083e8..bf0f1efd0f 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts @@ -5,9 +5,10 @@ * 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 {R3Reference} from '@angular/compiler'; +import {createR3ProviderExpression, R3DeclareDependencyMetadata, R3DependencyMetadata, R3ProviderExpression, R3Reference} from '@angular/compiler'; import * as o from '@angular/compiler/src/output/output_ast'; -import {AstValue} from '../../ast/ast_value'; + +import {AstObject, AstValue} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; export function wrapReference(wrapped: o.WrappedNodeExpr): R3Reference { @@ -29,3 +30,66 @@ export function parseEnum( } return enumValue; } + +/** + * Parse a dependency structure from an AST object. + */ +export function getDependency( + depObj: AstObject): R3DependencyMetadata { + const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute'); + const token = depObj.getOpaque('token'); + // Normally `attribute` is a string literal and so its `attributeNameType` is the same string + // literal. If the `attribute` is some other expression, the `attributeNameType` would be the + // `unknown` type. It is not possible to generate this when linking, since it only deals with JS + // and not typings. When linking the existence of the `attributeNameType` only acts as a marker to + // change the injection instruction that is generated, so we just pass the literal string + // `"unknown"`. + const attributeNameType = isAttribute ? o.literal('unknown') : null; + return { + token, + attributeNameType, + host: depObj.has('host') && depObj.getBoolean('host'), + optional: depObj.has('optional') && depObj.getBoolean('optional'), + self: depObj.has('self') && depObj.getBoolean('self'), + skipSelf: depObj.has('skipSelf') && depObj.getBoolean('skipSelf'), + }; +} + + +/** + * Return an `R3ProviderExpression` that represents either the extracted type reference expression + * from a `forwardRef` function call, or the type itself. + * + * For example, the expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note + * that this expression is required to be wrapped in a closure, as otherwise the forward reference + * would be resolved before initialization. + * + * If there is no forwardRef call expression then we just return the opaque type. + */ +export function extractForwardRef(expr: AstValue): + R3ProviderExpression> { + if (!expr.isCallExpression()) { + return createR3ProviderExpression(expr.getOpaque(), /* isForwardRef */ false); + } + + const callee = expr.getCallee(); + if (callee.getSymbolName() !== 'forwardRef') { + throw new FatalLinkerError( + callee.expression, + 'Unsupported expression, expected a `forwardRef()` call or a type reference'); + } + + const args = expr.getArguments(); + if (args.length !== 1) { + throw new FatalLinkerError( + expr, 'Unsupported `forwardRef(fn)` call, expected a single argument'); + } + + const wrapperFn = args[0] as AstValue; + if (!wrapperFn.isFunction()) { + throw new FatalLinkerError( + wrapperFn, 'Unsupported `forwardRef(fn)` call, expected its argument to be a function'); + } + + return createR3ProviderExpression(wrapperFn.getFunctionReturnValue().getOpaque(), true); +} diff --git a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts index ca10c691c8..2aecd76dc0 100644 --- a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts @@ -17,6 +17,7 @@ import {LinkerEnvironment} from '../../../src/file_linker/linker_environment'; import {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1'; import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_linker_1'; import {PartialFactoryLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_factory_linker_1'; +import {PartialInjectableLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_injectable_linker_1'; import {PartialInjectorLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_injector_linker_1'; import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector'; import {PartialNgModuleLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_ng_module_linker_1'; @@ -43,6 +44,7 @@ describe('PartialLinkerSelector', () => { expect(selector.supportsDeclaration('ɵɵngDeclareDirective')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareFactory')).toBe(true); + expect(selector.supportsDeclaration('ɵɵngDeclareInjectable')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareInjector')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareNgModule')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclarePipe')).toBe(true); @@ -66,6 +68,8 @@ describe('PartialLinkerSelector', () => { .toBeInstanceOf(PartialComponentLinkerVersion1); expect(selector.getLinker('ɵɵngDeclareFactory', '0.0.0-PLACEHOLDER')) .toBeInstanceOf(PartialFactoryLinkerVersion1); + expect(selector.getLinker('ɵɵngDeclareInjectable', '0.0.0-PLACEHOLDER')) + .toBeInstanceOf(PartialInjectableLinkerVersion1); expect(selector.getLinker('ɵɵngDeclareInjector', '0.0.0-PLACEHOLDER')) .toBeInstanceOf(PartialInjectorLinkerVersion1); expect(selector.getLinker('ɵɵngDeclareNgModule', '0.0.0-PLACEHOLDER')) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 78a775b519..5209aad0a8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -24,7 +24,7 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFl import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics, getUndecoratedClassWithAngularFeaturesDiagnostic} from './diagnostics'; import {compileDeclareFactory, compileNgFactoryDefField} from './factory'; import {generateSetClassMetadataCall} from './metadata'; -import {compileResults, createSourceSpan, findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, resolveProvidersRequiringFactory, toFactoryMetadata, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util'; +import {compileResults, createSourceSpan, findAngularDecorator, getConstructorDependencies, isAngularDecorator, readBaseClass, resolveProvidersRequiringFactory, toFactoryMetadata, tryUnwrapForwardRef, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util'; const EMPTY_OBJECT: {[key: string]: string} = {}; const FIELD_DECORATORS = [ @@ -528,7 +528,7 @@ export function extractQueryMetadata( ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`); } const first = name === 'ViewChild' || name === 'ContentChild'; - const node = unwrapForwardRef(args[0], reflector); + const node = tryUnwrapForwardRef(args[0], reflector) ?? args[0]; const arg = evaluator.evaluate(node); /** Whether or not this query should collect only static results (see view/api.ts) */ diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/factory.ts b/packages/compiler-cli/src/ngtsc/annotations/src/factory.ts index ae6c44e7ed..d20c307f4e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/factory.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/factory.ts @@ -10,6 +10,8 @@ import {compileDeclareFactoryFunction, compileFactoryFunction, R3FactoryMetadata import {CompileResult} from '../../transform'; +export type CompileFactoryFn = (metadata: R3FactoryMetadata) => CompileResult; + export function compileNgFactoryDefField(metadata: R3FactoryMetadata): CompileResult { const res = compileFactoryFunction(metadata); return {name: 'ɵfac', initializer: res.expression, statements: res.statements, type: res.type}; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 63636a2ce6..e400bef4f8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileInjectable as compileIvyInjectable, Expression, FactoryTarget, LiteralExpr, R3DependencyMetadata, R3FactoryMetadata, R3InjectableMetadata, Statement, WrappedNodeExpr} from '@angular/compiler'; +import {compileDeclareInjectableFromMetadata, compileInjectable, createR3ProviderExpression, Expression, FactoryTarget, LiteralExpr, R3CompiledExpression, R3DependencyMetadata, R3InjectableMetadata, R3ProviderExpression, Statement, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -16,9 +16,9 @@ import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; -import {compileDeclareFactory, compileNgFactoryDefField} from './factory'; +import {compileDeclareFactory, CompileFactoryFn, compileNgFactoryDefField} from './factory'; import {generateSetClassMetadataCall} from './metadata'; -import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, toFactoryMetadata, unwrapConstructorDependencies, unwrapForwardRef, validateConstructorDependencies, wrapTypeReference} from './util'; +import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, toFactoryMetadata, tryUnwrapForwardRef, unwrapConstructorDependencies, validateConstructorDependencies, wrapTypeReference} from './util'; export interface InjectableHandlerData { meta: R3InjectableMetadata; @@ -28,7 +28,7 @@ export interface InjectableHandlerData { } /** - * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. + * Adapts the `compileInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ export class InjectableDecoratorHandler implements DecoratorHandler { @@ -95,45 +95,25 @@ export class InjectableDecoratorHandler implements } compileFull(node: ClassDeclaration, analysis: Readonly): CompileResult[] { - const res = compileIvyInjectable(analysis.meta); - const statements = res.statements; - const results: CompileResult[] = []; - - if (analysis.needsFactory) { - const meta = analysis.meta; - const factoryRes = compileNgFactoryDefField( - toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable)); - if (analysis.metadataStmt !== null) { - factoryRes.statements.push(analysis.metadataStmt); - } - results.push(factoryRes); - } - - const ɵprov = this.reflector.getMembersOfClass(node).find(member => member.name === 'ɵprov'); - if (ɵprov !== undefined && this.errorOnDuplicateProv) { - throw new FatalDiagnosticError( - ErrorCode.INJECTABLE_DUPLICATE_PROV, ɵprov.nameNode || ɵprov.node || node, - 'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.'); - } - - if (ɵprov === undefined) { - // Only add a new ɵprov if there is not one already - results.push({name: 'ɵprov', initializer: res.expression, statements, type: res.type}); - } - - - return results; + return this.compile( + compileNgFactoryDefField, meta => compileInjectable(meta, false), node, analysis); } compilePartial(node: ClassDeclaration, analysis: Readonly): CompileResult[] { - const res = compileIvyInjectable(analysis.meta); - const statements = res.statements; + return this.compile( + compileDeclareFactory, compileDeclareInjectableFromMetadata, node, analysis); + } + + private compile( + compileFactoryFn: CompileFactoryFn, + compileInjectableFn: (meta: R3InjectableMetadata) => R3CompiledExpression, + node: ClassDeclaration, analysis: Readonly): CompileResult[] { const results: CompileResult[] = []; if (analysis.needsFactory) { const meta = analysis.meta; - const factoryRes = compileDeclareFactory( + const factoryRes = compileFactoryFn( toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable)); if (analysis.metadataStmt !== null) { factoryRes.statements.push(analysis.metadataStmt); @@ -150,7 +130,9 @@ export class InjectableDecoratorHandler implements if (ɵprov === undefined) { // Only add a new ɵprov if there is not one already - results.push({name: 'ɵprov', initializer: res.expression, statements, type: res.type}); + const res = compileInjectableFn(analysis.meta); + results.push( + {name: 'ɵprov', initializer: res.expression, statements: res.statements, type: res.type}); } return results; @@ -159,7 +141,7 @@ export class InjectableDecoratorHandler implements /** * Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the - * input metadata needed to run `compileIvyInjectable`. + * input metadata needed to run `compileInjectable`. * * A `null` return value indicates this is @Injectable has invalid data. */ @@ -181,7 +163,7 @@ function extractInjectableMetadata( type, typeArgumentCount, internalType, - providedIn: new LiteralExpr(null), + providedIn: createR3ProviderExpression(new LiteralExpr(null), false), }; } else if (decorator.args.length === 1) { const metaNode = decorator.args[0]; @@ -196,12 +178,12 @@ function extractInjectableMetadata( // Resolve the fields of the literal into a map of field name to expression. const meta = reflectObjectLiteral(metaNode); - let providedIn: Expression = new LiteralExpr(null); - if (meta.has('providedIn')) { - providedIn = new WrappedNodeExpr(meta.get('providedIn')!); - } - let userDeps: R3DependencyMetadata[]|undefined = undefined; + const providedIn = meta.has('providedIn') ? + getProviderExpression(meta.get('providedIn')!, reflector) : + createR3ProviderExpression(new LiteralExpr(null), false); + + let deps: R3DependencyMetadata[]|undefined = undefined; if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) { const depsExpr = meta.get('deps')!; if (!ts.isArrayLiteralExpression(depsExpr)) { @@ -209,58 +191,42 @@ function extractInjectableMetadata( ErrorCode.VALUE_NOT_LITERAL, depsExpr, `@Injectable deps metadata must be an inline array`); } - userDeps = depsExpr.elements.map(dep => getDep(dep, reflector)); + deps = depsExpr.elements.map(dep => getDep(dep, reflector)); } + const result: R3InjectableMetadata = {name, type, typeArgumentCount, internalType, providedIn}; if (meta.has('useValue')) { - return { - name, - type, - typeArgumentCount, - internalType, - providedIn, - useValue: new WrappedNodeExpr(unwrapForwardRef(meta.get('useValue')!, reflector)), - }; + result.useValue = getProviderExpression(meta.get('useValue')!, reflector); } else if (meta.has('useExisting')) { - return { - name, - type, - typeArgumentCount, - internalType, - providedIn, - useExisting: new WrappedNodeExpr(unwrapForwardRef(meta.get('useExisting')!, reflector)), - }; + result.useExisting = getProviderExpression(meta.get('useExisting')!, reflector); } else if (meta.has('useClass')) { - return { - name, - type, - typeArgumentCount, - internalType, - providedIn, - useClass: new WrappedNodeExpr(unwrapForwardRef(meta.get('useClass')!, reflector)), - userDeps, - }; + result.useClass = getProviderExpression(meta.get('useClass')!, reflector); + result.deps = deps; } else if (meta.has('useFactory')) { - // useFactory is special - the 'deps' property must be analyzed. - const factory = new WrappedNodeExpr(meta.get('useFactory')!); - return { - name, - type, - typeArgumentCount, - internalType, - providedIn, - useFactory: factory, - userDeps, - }; - } else { - return {name, type, typeArgumentCount, internalType, providedIn}; + result.useFactory = new WrappedNodeExpr(meta.get('useFactory')!); + result.deps = deps; } + return result; } else { throw new FatalDiagnosticError( ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], 'Too many arguments to @Injectable'); } } +/** + * Get the `R3ProviderExpression` for this `expression`. + * + * The `useValue`, `useExisting` and `useClass` properties might be wrapped in a `ForwardRef`, which + * needs to be unwrapped. This function will do that unwrapping and set a flag on the returned + * object to indicate whether the value needed unwrapping. + */ +function getProviderExpression( + expression: ts.Expression, reflector: ReflectionHost): R3ProviderExpression { + const forwardRefValue = tryUnwrapForwardRef(expression, reflector); + return createR3ProviderExpression( + new WrappedNodeExpr(forwardRefValue ?? expression), forwardRefValue !== null); +} + function extractInjectableCtorDeps( clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator, reflector: ReflectionHost, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 5820adbb46..098b18391d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -332,36 +332,40 @@ function expandForwardRef(arg: ts.Expression): ts.Expression|null { } } + /** - * Possibly resolve a forwardRef() expression into the inner value. + * If the given `node` is a forwardRef() expression then resolve its inner value, otherwise return + * `null`. * * @param node the forwardRef() expression to resolve * @param reflector a ReflectionHost - * @returns the resolved expression, if the original expression was a forwardRef(), or the original - * expression otherwise + * @returns the resolved expression, if the original expression was a forwardRef(), or `null` + * otherwise. */ -export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression { +export function tryUnwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression| + null { node = unwrapExpression(node); if (!ts.isCallExpression(node) || node.arguments.length !== 1) { - return node; + return null; } const fn = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression; if (!ts.isIdentifier(fn)) { - return node; + return null; } const expr = expandForwardRef(node.arguments[0]); if (expr === null) { - return node; + return null; } + const imp = reflector.getImportOfIdentifier(fn); if (imp === null || imp.from !== '@angular/core' || imp.name !== 'forwardRef') { - return node; - } else { - return expr; + return null; } + + return expr; } /** diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/GOLDEN_PARTIAL.js index aebc857eab..796ca1b0a7 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/ng_modules/GOLDEN_PARTIAL.js @@ -167,7 +167,7 @@ import * as i0 from "@angular/core"; export class Thing { } Thing.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Thing, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -Thing.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: Thing, factory: Thing.ɵfac }); +Thing.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Thing }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Thing, [{ type: Injectable }], null, null); })(); @@ -178,14 +178,14 @@ export class BaseService { ; } BaseService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: BaseService, deps: [{ token: Thing }], target: i0.ɵɵFactoryTarget.Injectable }); -BaseService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: BaseService, factory: BaseService.ɵfac }); +BaseService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: BaseService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BaseService, [{ type: Injectable }], function () { return [{ type: Thing }]; }, null); })(); export class ChildService extends BaseService { } ChildService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); -ChildService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ChildService, factory: ChildService.ɵfac }); +ChildService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ChildService, [{ type: Injectable }], null, null); })(); @@ -469,7 +469,7 @@ import * as i0 from "@angular/core"; export class Service { } Service.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -Service.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: Service, factory: Service.ɵfac }); +Service.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Service, [{ type: Injectable }], null, null); })(); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/GOLDEN_PARTIAL.js index 1b15eacfd1..5ecfc3b03b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/GOLDEN_PARTIAL.js @@ -6,7 +6,7 @@ import * as i0 from "@angular/core"; export class MyService { } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: MyService.ɵfac }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable }], null, null); })(); @@ -80,7 +80,7 @@ export class MyService { constructor(dep) { } } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [{ token: MyDependency }], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: MyService.ɵfac }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable }], function () { return [{ type: MyDependency }]; }, null); })(); @@ -111,7 +111,7 @@ export class MyService { constructor(dep, optionalDep) { } } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [{ token: MyDependency }, { token: MyOptionalDependency, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: MyService.ɵfac }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable }], function () { return [{ type: MyDependency }, { type: MyOptionalDependency, decorators: [{ @@ -144,7 +144,7 @@ function alternateFactory() { export class MyService { } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: function () { return alternateFactory(); }, providedIn: 'root' }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, providedIn: 'root', useFactory: alternateFactory }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable, args: [{ providedIn: 'root', useFactory: alternateFactory }] @@ -171,12 +171,7 @@ class MyAlternateService { export class MyService { } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: function MyService_Factory(t) { let r = null; if (t) { - r = new t(); - } - else { - r = (() => new MyAlternateService())(i0.ɵɵinject(SomeDep)); - } return r; }, providedIn: 'root' }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, providedIn: 'root', useFactory: () => new MyAlternateService(), deps: [{ token: SomeDep }] }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable, args: [{ providedIn: 'root', useFactory: () => new MyAlternateService(), deps: [SomeDep] }] @@ -199,14 +194,14 @@ import * as i0 from "@angular/core"; class MyAlternateService { } MyAlternateService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyAlternateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyAlternateService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyAlternateService, factory: MyAlternateService.ɵfac }); +MyAlternateService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyAlternateService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyAlternateService, [{ type: Injectable }], null, null); })(); export class MyService { } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: function (t) { return MyAlternateService.ɵfac(t); }, providedIn: 'root' }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, providedIn: 'root', useClass: MyAlternateService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable, args: [{ providedIn: 'root', useClass: MyAlternateService }] @@ -231,19 +226,14 @@ class SomeDep { class MyAlternateService { } MyAlternateService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyAlternateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyAlternateService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyAlternateService, factory: MyAlternateService.ɵfac }); +MyAlternateService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyAlternateService }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyAlternateService, [{ type: Injectable }], null, null); })(); export class MyService { } MyService.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -MyService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyService, factory: function MyService_Factory(t) { let r = null; if (t) { - r = new t(); - } - else { - r = new MyAlternateService(i0.ɵɵinject(SomeDep)); - } return r; }, providedIn: 'root' }); +MyService.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, providedIn: 'root', useClass: MyAlternateService, deps: [{ token: SomeDep }] }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyService, [{ type: Injectable, args: [{ providedIn: 'root', useClass: MyAlternateService, deps: [SomeDep] }] @@ -266,7 +256,7 @@ import * as i0 from "@angular/core"; class SomeProvider { } SomeProvider.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -SomeProvider.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SomeProvider, factory: function (t) { return SomeProviderImpl.ɵfac(t); }, providedIn: 'root' }); +SomeProvider.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeProvider, providedIn: 'root', useClass: i0.forwardRef(function () { return SomeProviderImpl; }) }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SomeProvider, [{ type: Injectable, args: [{ providedIn: 'root', useClass: forwardRef(() => SomeProviderImpl) }] @@ -274,7 +264,7 @@ SomeProvider.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SomeProvide class SomeProviderImpl extends SomeProvider { } SomeProviderImpl.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeProviderImpl, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); -SomeProviderImpl.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SomeProviderImpl, factory: SomeProviderImpl.ɵfac }); +SomeProviderImpl.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeProviderImpl }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SomeProviderImpl, [{ type: Injectable }], null, null); })(); @@ -284,6 +274,55 @@ SomeProviderImpl.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SomePro ****************************************************************************************************/ export {}; +/**************************************************************************************************** + * PARTIAL FILE: providedin_forwardref.js + ****************************************************************************************************/ +import { forwardRef, Injectable, NgModule } from '@angular/core'; +import * as i0 from "@angular/core"; +export class Dep { +} +Dep.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Dep, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); +Dep.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Dep }); +(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Dep, [{ + type: Injectable + }], null, null); })(); +export class Service { + constructor(dep) { } +} +Service.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service, deps: [{ token: Dep }], target: i0.ɵɵFactoryTarget.Injectable }); +Service.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service, providedIn: i0.forwardRef(function () { return Mod; }) }); +(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Service, [{ + type: Injectable, + args: [{ providedIn: forwardRef(() => Mod) }] + }], function () { return [{ type: Dep }]; }, null); })(); +export class Mod { +} +Mod.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Mod, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +Mod.ɵmod = i0.ɵɵngDeclareNgModule({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Mod }); +Mod.ɵinj = i0.ɵɵngDeclareInjector({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Mod }); +(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Mod, [{ + type: NgModule + }], null, null); })(); + +/**************************************************************************************************** + * PARTIAL FILE: providedin_forwardref.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class Dep { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵprov: i0.ɵɵInjectableDeclaration; +} +export declare class Service { + constructor(dep: Dep); + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵprov: i0.ɵɵInjectableDeclaration; +} +export declare class Mod { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + /**************************************************************************************************** * PARTIAL FILE: pipe_and_injectable.js ****************************************************************************************************/ @@ -292,7 +331,7 @@ import * as i0 from "@angular/core"; class Service { } Service.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); -Service.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: Service, factory: Service.ɵfac }); +Service.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: Service }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Service, [{ type: Injectable }], null, null); })(); @@ -304,7 +343,7 @@ export class MyPipe { } MyPipe.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyPipe, deps: [{ token: Service }], target: i0.ɵɵFactoryTarget.Pipe }); MyPipe.ɵpipe = i0.ɵɵngDeclarePipe({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyPipe, name: "myPipe" }); -MyPipe.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyPipe, factory: MyPipe.ɵfac }); +MyPipe.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyPipe }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyPipe, [{ type: Injectable }, { @@ -319,7 +358,7 @@ export class MyOtherPipe { } MyOtherPipe.ɵfac = i0.ɵɵngDeclareFactory({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyOtherPipe, deps: [{ token: Service }], target: i0.ɵɵFactoryTarget.Pipe }); MyOtherPipe.ɵpipe = i0.ɵɵngDeclarePipe({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyOtherPipe, name: "myOtherPipe" }); -MyOtherPipe.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: MyOtherPipe, factory: MyOtherPipe.ɵfac }); +MyOtherPipe.ɵprov = i0.ɵɵngDeclareInjectable({ version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyOtherPipe }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyOtherPipe, [{ type: Pipe, args: [{ name: 'myOtherPipe' }] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/TEST_CASES.json index 7968fc1008..33a537c242 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/TEST_CASES.json @@ -137,6 +137,20 @@ } ] }, + { + "description": "should support forward refs in a providedIn clause", + "inputFiles": [ + "providedin_forwardref.ts" + ], + "expectations": [ + { + "failureMessage": "Incorrect factory definition", + "files": [ + "providedin_forwardref.js" + ] + } + ] + }, { "description": "should have the pipe factory take precedence over the injectable factory, if a class has multiple decorators", "inputFiles": [ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.js new file mode 100644 index 0000000000..7af903b521 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.js @@ -0,0 +1,8 @@ +Service.ɵfac = function Service_Factory(t) { return new (t || Service)($i0$.ɵɵinject(Dep)); }; +Service.ɵprov = /*@__PURE__*/ $i0$.ɵɵdefineInjectable({ token: Service, factory: Service.ɵfac, providedIn: $i0$.forwardRef(function () { return Mod; }) }); +(function () { (typeof ngDevMode === "undefined" || ngDevMode) && $i0$.ɵsetClassMetadata(Service, [{ + type: Injectable, + args: [{ providedIn: forwardRef(() => Mod) }] +}], function () { return [{ type: Dep }]; }, null); })(); +export class Mod { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.ts new file mode 100644 index 0000000000..03669fca44 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_di/di/providedin_forwardref.ts @@ -0,0 +1,12 @@ +import {forwardRef, Injectable, NgModule} from '@angular/core'; + +@Injectable() +export class Dep { +} +@Injectable({providedIn: forwardRef(() => Mod)}) +export class Service { + constructor(dep: Dep) {} +} +@NgModule() +export class Mod { +} \ No newline at end of file diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 4968375eb3..cf8700ab67 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -208,11 +208,11 @@ function allTests(os: string) { expect(jsContents) .toContain( 'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };'); - expect(jsContents).toContain('providedIn: forwardRef(function () { return Mod; }) })'); + expect(jsContents).toContain('providedIn: i0.forwardRef(function () { return Mod; }) })'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); + expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); expect(dtsContents).toContain('i0.ɵɵFactoryDeclaration;'); diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index e194e6183e..b111371da6 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -36,6 +36,11 @@ export function identifierName(compileIdentifier: CompileIdentifierMetadata|null if (ref['__anonymousType']) { return ref['__anonymousType']; } + if (ref['__forward_ref__']) { + // We do not want to try to stringify a `forwardRef()` function because that would cause the + // inner function to be evaluated too early, defeating the whole point of the `forwardRef`. + return '__forward_ref__'; + } let identifier = stringify(ref); if (identifier.indexOf('(') >= 0) { // case: anonymous functions! diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 9f1bc70dc6..89668ee82e 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -107,6 +107,7 @@ export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBin export {compileDeclareComponentFromMetadata} from './render3/partial/component'; export {compileDeclareDirectiveFromMetadata} from './render3/partial/directive'; export {compileDeclareFactoryFunction} from './render3/partial/factory'; +export {compileDeclareInjectableFromMetadata} from './render3/partial/injectable'; export {compileDeclareInjectorFromMetadata} from './render3/partial/injector'; export {compileDeclareNgModuleFromMetadata} from './render3/partial/ng_module'; export {compileDeclarePipeFromMetadata} from './render3/partial/pipe'; diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 5343e4583c..3bfb7d2fc1 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -33,6 +33,8 @@ export interface CompilerFacade { angularCoreEnv: CoreEnvironment, sourceMapUrl: string, declaration: R3DeclarePipeFacade): any; compileInjectable( angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectableMetadataFacade): any; + compileInjectableDeclaration( + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DeclareInjectableFacade): any; compileInjector( angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectorMetadataFacade): any; compileInjectorDeclaration( @@ -80,7 +82,9 @@ export type StringMapWithRename = { [key: string]: string|[string, string]; }; -export type Provider = any; +export type Provider = unknown; +export type Type = Function; +export type OpaqueValue = unknown; export enum FactoryTarget { Directive = 0, @@ -91,7 +95,7 @@ export enum FactoryTarget { } export interface R3DependencyMetadataFacade { - token: unknown; + token: OpaqueValue; attribute: string|null; host: boolean; optional: boolean; @@ -100,7 +104,7 @@ export interface R3DependencyMetadataFacade { } export interface R3DeclareDependencyMetadataFacade { - token: unknown; + token: OpaqueValue; attribute?: boolean; host?: boolean; optional?: boolean; @@ -110,25 +114,25 @@ export interface R3DeclareDependencyMetadataFacade { export interface R3PipeMetadataFacade { name: string; - type: any; + type: Type; pipeName: string; pure: boolean; } export interface R3InjectableMetadataFacade { name: string; - type: any; + type: Type; typeArgumentCount: number; - providedIn: any; - useClass?: any; - useFactory?: any; - useExisting?: any; - useValue?: any; - userDeps?: R3DependencyMetadataFacade[]; + providedIn?: Type|'root'|'platform'|'any'|null; + useClass?: OpaqueValue; + useFactory?: OpaqueValue; + useExisting?: OpaqueValue; + useValue?: OpaqueValue; + deps?: R3DependencyMetadataFacade[]; } export interface R3NgModuleMetadataFacade { - type: any; + type: Type; bootstrap: Function[]; declarations: Function[]; imports: Function[]; @@ -139,19 +143,19 @@ export interface R3NgModuleMetadataFacade { export interface R3InjectorMetadataFacade { name: string; - type: any; - providers: any[]; - imports: any[]; + type: Type; + providers: Provider[]; + imports: OpaqueValue[]; } export interface R3DirectiveMetadataFacade { name: string; - type: any; + type: Type; typeSourceSpan: ParseSourceSpan; selector: string|null; queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; - propMetadata: {[key: string]: any[]}; + propMetadata: {[key: string]: OpaqueValue[]}; lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; @@ -164,7 +168,7 @@ export interface R3DirectiveMetadataFacade { export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { template: string; preserveWhitespaces: boolean; - animations: any[]|undefined; + animations: OpaqueValue[]|undefined; pipes: Map; directives: R3UsedDirectiveMetadata[]; styles: string[]; @@ -174,11 +178,9 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { changeDetection?: ChangeDetectionStrategy; } -export type OpaqueValue = unknown; - export interface R3DeclareDirectiveFacade { selector?: string; - type: Function; + type: Type; inputs?: {[classPropertyName: string]: string|[string, string]}; outputs?: {[classPropertyName: string]: string}; host?: { @@ -229,18 +231,28 @@ export interface R3UsedDirectiveMetadata { export interface R3FactoryDefMetadataFacade { name: string; - type: any; + type: Type; typeArgumentCount: number; deps: R3DependencyMetadataFacade[]|null; target: FactoryTarget; } export interface R3DeclareFactoryFacade { - type: Function; + type: Type; deps: R3DeclareDependencyMetadataFacade[]|null; target: FactoryTarget; } +export interface R3DeclareInjectableFacade { + type: Type; + providedIn?: Type|'root'|'platform'|'any'|null; + useClass?: OpaqueValue; + useFactory?: OpaqueValue; + useExisting?: OpaqueValue; + useValue?: OpaqueValue; + deps?: R3DeclareDependencyMetadataFacade[]; +} + export enum ViewEncapsulation { Emulated = 0, // Historically the 1 value was for `Native` encapsulation which has been removed as of v11. @@ -253,10 +265,10 @@ export type ChangeDetectionStrategy = number; export interface R3QueryMetadataFacade { propertyName: string; first: boolean; - predicate: any|string[]; + predicate: OpaqueValue|string[]; descendants: boolean; emitDistinctChangesOnly: boolean; - read: any|null; + read: OpaqueValue|null; static: boolean; } @@ -271,13 +283,13 @@ export interface R3DeclareQueryMetadataFacade { } export interface R3DeclareInjectorFacade { - type: Function; + type: Type; imports?: OpaqueValue[]; providers?: OpaqueValue[]; } export interface R3DeclareNgModuleFacade { - type: Function; + type: Type; bootstrap?: OpaqueValue[]|(() => OpaqueValue[]); declarations?: OpaqueValue[]|(() => OpaqueValue[]); imports?: OpaqueValue[]|(() => OpaqueValue[]); @@ -287,7 +299,7 @@ export interface R3DeclareNgModuleFacade { } export interface R3DeclarePipeFacade { - type: Function; + type: Type; name: string; pure?: boolean; } diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index 82e893d00b..1855614334 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -66,9 +66,6 @@ export class Identifiers { static directiveInject: o.ExternalReference = {name: 'ɵɵdirectiveInject', moduleName: CORE}; static INJECTOR: o.ExternalReference = {name: 'INJECTOR', moduleName: CORE}; static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE}; - static ɵɵdefineInjectable: o.ExternalReference = {name: 'ɵɵdefineInjectable', moduleName: CORE}; - static InjectableDeclaration: - o.ExternalReference = {name: 'ɵɵInjectableDeclaration', moduleName: CORE}; static ViewEncapsulation: o.ExternalReference = { name: 'ViewEncapsulation', moduleName: CORE, diff --git a/packages/compiler/src/injectable_compiler.ts b/packages/compiler/src/injectable_compiler.ts index 272a4306c8..533577a6dd 100644 --- a/packages/compiler/src/injectable_compiler.ts +++ b/packages/compiler/src/injectable_compiler.ts @@ -7,16 +7,14 @@ */ import {StaticSymbol} from './aot/static_symbol'; -import {CompileInjectableMetadata, CompileNgModuleMetadata, CompileProviderMetadata, identifierName} from './compile_metadata'; +import {CompileInjectableMetadata, identifierName} from './compile_metadata'; import {CompileReflector} from './compile_reflector'; -import {InjectFlags, NodeFlags} from './core'; +import {InjectFlags} from './core'; import {Identifiers} from './identifiers'; import * as o from './output/output_ast'; import {convertValueToOutputAst} from './output/value_util'; -import {typeSourceSpan} from './parse_util'; -import {NgModuleProviderAnalyzer} from './provider_analyzer'; +import {Identifiers as R3} from './render3/r3_identifiers'; import {OutputContext} from './util'; -import {componentFactoryResolverProviderDef, depDef, providerDef} from './view_compiler/provider_compiler'; type MapEntry = { key: string, @@ -116,8 +114,7 @@ export class InjectableCompiler { mapEntry('token', ctx.importExpr(injectable.type.reference)), mapEntry('providedIn', providedIn), ]; - return o.importExpr(Identifiers.ɵɵdefineInjectable) - .callFn([o.literalMap(def)], undefined, true); + return o.importExpr(R3.ɵɵdefineInjectable).callFn([o.literalMap(def)], undefined, true); } compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void { diff --git a/packages/compiler/src/injectable_compiler_2.ts b/packages/compiler/src/injectable_compiler_2.ts index a7ec24b676..d845e7e56f 100644 --- a/packages/compiler/src/injectable_compiler_2.ts +++ b/packages/compiler/src/injectable_compiler_2.ts @@ -6,32 +6,62 @@ * found in the LICENSE file at https://angular.io/license */ -import {Identifiers} from './identifiers'; import * as o from './output/output_ast'; +import {generateForwardRef} from './render3/partial/util'; import {compileFactoryFunction, FactoryTarget, R3DependencyMetadata, R3FactoryDelegateType, R3FactoryMetadata} from './render3/r3_factory'; -import {R3Reference, typeWithParameters} from './render3/util'; +import {Identifiers} from './render3/r3_identifiers'; +import {R3CompiledExpression, R3Reference, typeWithParameters} from './render3/util'; import {DefinitionMap} from './render3/view/util'; -export interface InjectableDef { - expression: o.Expression; - type: o.Type; - statements: o.Statement[]; -} - export interface R3InjectableMetadata { name: string; type: R3Reference; internalType: o.Expression; typeArgumentCount: number; - providedIn: o.Expression; - useClass?: o.Expression; + providedIn: R3ProviderExpression; + useClass?: R3ProviderExpression; useFactory?: o.Expression; - useExisting?: o.Expression; - useValue?: o.Expression; - userDeps?: R3DependencyMetadata[]; + useExisting?: R3ProviderExpression; + useValue?: R3ProviderExpression; + deps?: R3DependencyMetadata[]; } -export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { +/** + * An expression used when instantiating an injectable. + * + * This is the type of the `useClass`, `useExisting` and `useValue` properties of + * `R3InjectableMetadata` since those can refer to types that may eagerly reference types that have + * not yet been defined. + */ +export interface R3ProviderExpression { + /** + * The expression that is used to instantiate the Injectable. + */ + expression: T; + /** + * If true, then the `expression` contains a reference to something that has not yet been + * defined. + * + * This means that the expression must not be eagerly evaluated. Instead it must be wrapped in a + * function closure that will be evaluated lazily to allow the definition of the expression to be + * evaluated first. + * + * In some cases the expression will naturally be placed inside such a function closure, such as + * in a fully compiled factory function. In those case nothing more needs to be done. + * + * But in other cases, such as partial-compilation the expression will be located in top level + * code so will need to be wrapped in a function that is passed to a `forwardRef()` call. + */ + isForwardRef: boolean; +} + +export function createR3ProviderExpression( + expression: T, isForwardRef: boolean): R3ProviderExpression { + return {expression, isForwardRef}; +} + +export function compileInjectable( + meta: R3InjectableMetadata, resolveForwardRefs: boolean): R3CompiledExpression { let result: {expression: o.Expression, statements: o.Statement[]}|null = null; const factoryMeta: R3FactoryMetadata = { @@ -51,32 +81,36 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { // A special case exists for useClass: Type where Type is the injectable type itself and no // deps are specified, in which case 'useClass' is effectively ignored. - const useClassOnSelf = meta.useClass.isEquivalent(meta.internalType); + const useClassOnSelf = meta.useClass.expression.isEquivalent(meta.internalType); let deps: R3DependencyMetadata[]|undefined = undefined; - if (meta.userDeps !== undefined) { - deps = meta.userDeps; + if (meta.deps !== undefined) { + deps = meta.deps; } if (deps !== undefined) { // factory: () => new meta.useClass(...deps) result = compileFactoryFunction({ ...factoryMeta, - delegate: meta.useClass, + delegate: meta.useClass.expression, delegateDeps: deps, delegateType: R3FactoryDelegateType.Class, }); } else if (useClassOnSelf) { result = compileFactoryFunction(factoryMeta); } else { - result = delegateToFactory( - meta.type.value as o.WrappedNodeExpr, meta.useClass as o.WrappedNodeExpr); + result = { + statements: [], + expression: delegateToFactory( + meta.type.value as o.WrappedNodeExpr, + meta.useClass.expression as o.WrappedNodeExpr, resolveForwardRefs) + }; } } else if (meta.useFactory !== undefined) { - if (meta.userDeps !== undefined) { + if (meta.deps !== undefined) { result = compileFactoryFunction({ ...factoryMeta, delegate: meta.useFactory, - delegateDeps: meta.userDeps || [], + delegateDeps: meta.deps || [], delegateType: R3FactoryDelegateType.Function, }); } else { @@ -91,17 +125,21 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { // value is undefined. result = compileFactoryFunction({ ...factoryMeta, - expression: meta.useValue, + expression: meta.useValue.expression, }); } else if (meta.useExisting !== undefined) { // useExisting is an `inject` call on the existing token. result = compileFactoryFunction({ ...factoryMeta, - expression: o.importExpr(Identifiers.inject).callFn([meta.useExisting]), + expression: o.importExpr(Identifiers.inject).callFn([meta.useExisting.expression]), }); } else { - result = delegateToFactory( - meta.type.value as o.WrappedNodeExpr, meta.internalType as o.WrappedNodeExpr); + result = { + statements: [], + expression: delegateToFactory( + meta.type.value as o.WrappedNodeExpr, meta.internalType as o.WrappedNodeExpr, + resolveForwardRefs) + }; } const token = meta.internalType; @@ -112,32 +150,59 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { injectableProps.set('factory', result.expression); // Only generate providedIn property if it has a non-null value - if ((meta.providedIn as o.LiteralExpr).value !== null) { - injectableProps.set('providedIn', meta.providedIn); + if ((meta.providedIn.expression as o.LiteralExpr).value !== null) { + injectableProps.set( + 'providedIn', + meta.providedIn.isForwardRef ? generateForwardRef(meta.providedIn.expression) : + meta.providedIn.expression); } const expression = o.importExpr(Identifiers.ɵɵdefineInjectable) .callFn([injectableProps.toLiteralMap()], undefined, true); - const type = new o.ExpressionType(o.importExpr( - Identifiers.InjectableDeclaration, - [typeWithParameters(meta.type.type, meta.typeArgumentCount)])); - return { expression, - type, + type: createInjectableType(meta), statements: result.statements, }; } -function delegateToFactory(type: o.WrappedNodeExpr, internalType: o.WrappedNodeExpr) { - return { - statements: [], - // If types are the same, we can generate `factory: type.ɵfac` - // If types are different, we have to generate a wrapper function to ensure - // the internal type has been resolved (`factory: function(t) { return type.ɵfac(t); }`) - expression: type.node === internalType.node ? - internalType.prop('ɵfac') : - o.fn([new o.FnParam('t', o.DYNAMIC_TYPE)], [new o.ReturnStatement(internalType.callMethod( - 'ɵfac', [o.variable('t')]))]) - }; +export function createInjectableType(meta: R3InjectableMetadata) { + return new o.ExpressionType(o.importExpr( + Identifiers.InjectableDeclaration, + [typeWithParameters(meta.type.type, meta.typeArgumentCount)])); +} + +function delegateToFactory( + type: o.WrappedNodeExpr, internalType: o.WrappedNodeExpr, + unwrapForwardRefs: boolean): o.Expression { + if (type.node === internalType.node) { + // The types are the same, so we can simply delegate directly to the type's factory. + // ``` + // factory: type.ɵfac + // ``` + return internalType.prop('ɵfac'); + } + + if (!unwrapForwardRefs) { + // The type is not wrapped in a `forwardRef()`, so we create a simple factory function that + // accepts a sub-type as an argument. + // ``` + // factory: function(t) { return internalType.ɵfac(t); } + // ``` + return createFactoryFunction(internalType); + } + + // The internalType is actually wrapped in a `forwardRef()` so we need to resolve that before + // calling its factory. + // ``` + // factory: function(t) { return core.resolveForwardRef(type).ɵfac(t); } + // ``` + const unwrappedType = o.importExpr(Identifiers.resolveForwardRef).callFn([internalType]); + return createFactoryFunction(unwrappedType); +} + +function createFactoryFunction(type: o.Expression): o.FunctionExpr { + return o.fn( + [new o.FnParam('t', o.DYNAMIC_TYPE)], + [new o.ReturnStatement(type.callMethod('ɵfac', [o.variable('t')]))]); } diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 32496f8789..9e4033cffe 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -7,10 +7,10 @@ */ -import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, OpaqueValue, R3ComponentMetadataFacade, R3DeclareComponentFacade, R3DeclareDependencyMetadataFacade, R3DeclareDirectiveFacade, R3DeclareFactoryFacade, R3DeclareInjectorFacade, R3DeclareNgModuleFacade, R3DeclarePipeFacade, R3DeclareQueryMetadataFacade, R3DeclareUsedDirectiveFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface'; +import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, OpaqueValue, R3ComponentMetadataFacade, R3DeclareComponentFacade, R3DeclareDependencyMetadataFacade, R3DeclareDirectiveFacade, R3DeclareFactoryFacade, R3DeclareInjectableFacade, R3DeclareInjectorFacade, R3DeclareNgModuleFacade, R3DeclarePipeFacade, R3DeclareQueryMetadataFacade, R3DeclareUsedDirectiveFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface'; import {ConstantPool} from './constant_pool'; -import {ChangeDetectionStrategy, HostBinding, HostListener, Input, Output, Type, ViewEncapsulation} from './core'; -import {compileInjectable} from './injectable_compiler_2'; +import {ChangeDetectionStrategy, HostBinding, HostListener, Input, Output, ViewEncapsulation} from './core'; +import {compileInjectable, createR3ProviderExpression, R3ProviderExpression} from './injectable_compiler_2'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config'; import {DeclareVarStmt, Expression, literal, LiteralExpr, Statement, StmtModifier, WrappedNodeExpr} from './output/output_ast'; import {JitEvaluator} from './output/output_jit'; @@ -60,18 +60,41 @@ export class CompilerFacadeImpl implements CompilerFacade { compileInjectable( angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3InjectableMetadataFacade): any { - const {expression, statements} = compileInjectable({ - name: facade.name, - type: wrapReference(facade.type), - internalType: new WrappedNodeExpr(facade.type), - typeArgumentCount: facade.typeArgumentCount, - providedIn: computeProvidedIn(facade.providedIn), - useClass: wrapExpression(facade, USE_CLASS), - useFactory: wrapExpression(facade, USE_FACTORY), - useValue: wrapExpression(facade, USE_VALUE), - useExisting: wrapExpression(facade, USE_EXISTING), - userDeps: convertR3DependencyMetadataArray(facade.userDeps) || undefined, - }); + const {expression, statements} = compileInjectable( + { + name: facade.name, + type: wrapReference(facade.type), + internalType: new WrappedNodeExpr(facade.type), + typeArgumentCount: facade.typeArgumentCount, + providedIn: computeProvidedIn(facade.providedIn), + useClass: convertToProviderExpression(facade, USE_CLASS), + useFactory: wrapExpression(facade, USE_FACTORY), + useValue: convertToProviderExpression(facade, USE_VALUE), + useExisting: convertToProviderExpression(facade, USE_EXISTING), + deps: facade.deps?.map(convertR3DependencyMetadata), + }, + /* resolveForwardRefs */ true); + + return this.jitExpression(expression, angularCoreEnv, sourceMapUrl, statements); + } + + compileInjectableDeclaration( + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, + facade: R3DeclareInjectableFacade): any { + const {expression, statements} = compileInjectable( + { + name: facade.type.name, + type: wrapReference(facade.type), + internalType: new WrappedNodeExpr(facade.type), + typeArgumentCount: 0, + providedIn: computeProvidedIn(facade.providedIn), + useClass: convertToProviderExpression(facade, USE_CLASS), + useFactory: wrapExpression(facade, USE_FACTORY), + useValue: convertToProviderExpression(facade, USE_VALUE), + useExisting: convertToProviderExpression(facade, USE_EXISTING), + deps: facade.deps?.map(convertR3DeclareDependencyMetadata), + }, + /* resolveForwardRefs */ true); return this.jitExpression(expression, angularCoreEnv, sourceMapUrl, statements); } @@ -446,6 +469,22 @@ function parseJitTemplate( type R3DirectiveMetadataFacadeNoPropAndWhitespace = Pick>; +/** + * Convert the expression, if present to an `R3ProviderExpression`. + * + * In JIT mode we do not want the compiler to wrap the expression in a `forwardRef()` call because, + * if it is referencing a type that has not yet been defined, it will have already been wrapped in + * a `forwardRef()` - either by the application developer or during partial-compilation. Thus we can + * set `isForwardRef` to `false`. + */ +function convertToProviderExpression(obj: any, property: string): R3ProviderExpression|undefined { + if (obj.hasOwnProperty(property)) { + return createR3ProviderExpression(new WrappedNodeExpr(obj[property]), /* isForwardRef */ false); + } else { + return undefined; + } +} + function wrapExpression(obj: any, property: string): WrappedNodeExpr|undefined { if (obj.hasOwnProperty(property)) { return new WrappedNodeExpr(obj[property]); @@ -454,12 +493,12 @@ function wrapExpression(obj: any, property: string): WrappedNodeExpr|undefi } } -function computeProvidedIn(providedIn: Type|string|null|undefined): Expression { - if (providedIn == null || typeof providedIn === 'string') { - return new LiteralExpr(providedIn); - } else { - return new WrappedNodeExpr(providedIn); - } +function computeProvidedIn(providedIn: Function|string|null|undefined): R3ProviderExpression { + const expression = (providedIn == null || typeof providedIn === 'string') ? + new LiteralExpr(providedIn ?? null) : + new WrappedNodeExpr(providedIn); + // See `convertToProviderExpression()` for why `isForwardRef` is false. + return createR3ProviderExpression(expression, /* isForwardRef */ false); } function convertR3DependencyMetadataArray(facades: R3DependencyMetadataFacade[]|null| diff --git a/packages/compiler/src/render3/partial/api.ts b/packages/compiler/src/render3/partial/api.ts index 124e953291..5ea35f5e72 100644 --- a/packages/compiler/src/render3/partial/api.ts +++ b/packages/compiler/src/render3/partial/api.ts @@ -367,6 +367,53 @@ export enum FactoryTarget { NgModule = 4, } +/** + * Describes the shape of the object that the `ɵɵngDeclareInjectable()` function accepts. + * + * This interface serves primarily as documentation, as conformance to this interface is not + * enforced during linking. + */ +export interface R3DeclareInjectableMetadata extends R3PartialDeclaration { + /** + * If provided, specifies that the declared injectable belongs to a particular injector: + * - `InjectorType` such as `NgModule`, + * - `'root'` the root injector + * - `'any'` all injectors. + * If not provided, then it does not belong to any injector. Must be explicitly listed in the + * providers of an injector. + */ + providedIn?: o.Expression; + + /** + * If provided, an expression that evaluates to a class to use when creating an instance of this + * injectable. + */ + useClass?: o.Expression; + + /** + * If provided, an expression that evaluates to a function to use when creating an instance of + * this injectable. + */ + useFactory?: o.Expression; + + /** + * If provided, an expression that evaluates to a token of another injectable that this injectable + * aliases. + */ + useExisting?: o.Expression; + + /** + * If provided, an expression that evaluates to the value of the instance of this injectable. + */ + useValue?: o.Expression; + + /** + * An array of dependencies to support instantiating this injectable via `useClass` or + * `useFactory`. + */ + deps?: R3DeclareDependencyMetadata[]; +} + /** * Metadata indicating how a dependency should be injected into a factory. */ diff --git a/packages/compiler/src/render3/partial/component.ts b/packages/compiler/src/render3/partial/component.ts index d54f1e7b84..693af7ed62 100644 --- a/packages/compiler/src/render3/partial/component.ts +++ b/packages/compiler/src/render3/partial/component.ts @@ -18,7 +18,7 @@ import {DefinitionMap} from '../view/util'; import {R3DeclareComponentMetadata, R3DeclareUsedDirectiveMetadata} from './api'; import {createDirectiveDefinitionMap} from './directive'; -import {toOptionalLiteralArray} from './util'; +import {generateForwardRef, toOptionalLiteralArray} from './util'; /** @@ -163,7 +163,3 @@ function compileUsedPipeMetadata(meta: R3ComponentMetadata): o.LiteralMapExpr|nu } return o.literalMap(entries); } - -function generateForwardRef(expr: o.Expression): o.Expression { - return o.importExpr(R3.forwardRef).callFn([o.fn([], [new o.ReturnStatement(expr)])]); -} diff --git a/packages/compiler/src/render3/partial/factory.ts b/packages/compiler/src/render3/partial/factory.ts index 3a8e6d9eaa..52654e46d6 100644 --- a/packages/compiler/src/render3/partial/factory.ts +++ b/packages/compiler/src/render3/partial/factory.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ import * as o from '../../output/output_ast'; -import {createFactoryType, FactoryTarget, R3DependencyMetadata, R3FactoryMetadata} from '../r3_factory'; +import {createFactoryType, FactoryTarget, R3FactoryMetadata} from '../r3_factory'; import {Identifiers as R3} from '../r3_identifiers'; import {R3CompiledExpression} from '../util'; import {DefinitionMap} from '../view/util'; -import {R3DeclareDependencyMetadata, R3DeclareFactoryMetadata} from './api'; +import {R3DeclareFactoryMetadata} from './api'; +import {compileDependencies} from './util'; export function compileDeclareFactoryFunction(meta: R3FactoryMetadata): R3CompiledExpression { const definitionMap = new DefinitionMap(); @@ -27,35 +28,3 @@ export function compileDeclareFactoryFunction(meta: R3FactoryMetadata): R3Compil type: createFactoryType(meta), }; } - -function compileDependencies(deps: R3DependencyMetadata[]|'invalid'|null): o.LiteralExpr| - o.LiteralArrayExpr { - if (deps === 'invalid') { - return o.literal('invalid'); - } else if (deps === null) { - return o.literal(null); - } else { - return o.literalArr(deps.map(compileDependency)); - } -} - -function compileDependency(dep: R3DependencyMetadata): o.LiteralMapExpr { - const depMeta = new DefinitionMap(); - depMeta.set('token', dep.token); - if (dep.attributeNameType !== null) { - depMeta.set('attribute', o.literal(true)); - } - if (dep.host) { - depMeta.set('host', o.literal(true)); - } - if (dep.optional) { - depMeta.set('optional', o.literal(true)); - } - if (dep.self) { - depMeta.set('self', o.literal(true)); - } - if (dep.skipSelf) { - depMeta.set('skipSelf', o.literal(true)); - } - return depMeta.toLiteralMap(); -} diff --git a/packages/compiler/src/render3/partial/injectable.ts b/packages/compiler/src/render3/partial/injectable.ts new file mode 100644 index 0000000000..d7b3f47ed2 --- /dev/null +++ b/packages/compiler/src/render3/partial/injectable.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {createInjectableType, R3InjectableMetadata, R3ProviderExpression} from '../../injectable_compiler_2'; +import * as o from '../../output/output_ast'; +import {Identifiers as R3} from '../r3_identifiers'; +import {R3CompiledExpression} from '../util'; +import {DefinitionMap} from '../view/util'; + +import {R3DeclareInjectableMetadata} from './api'; +import {compileDependency, generateForwardRef} from './util'; + +/** + * Compile a Injectable declaration defined by the `R3InjectableMetadata`. + */ +export function compileDeclareInjectableFromMetadata(meta: R3InjectableMetadata): + R3CompiledExpression { + const definitionMap = createInjectableDefinitionMap(meta); + + const expression = o.importExpr(R3.declareInjectable).callFn([definitionMap.toLiteralMap()]); + const type = createInjectableType(meta); + + return {expression, type, statements: []}; +} + +/** + * Gathers the declaration fields for a Injectable into a `DefinitionMap`. + */ +export function createInjectableDefinitionMap(meta: R3InjectableMetadata): + DefinitionMap { + const definitionMap = new DefinitionMap(); + + definitionMap.set('version', o.literal('0.0.0-PLACEHOLDER')); + definitionMap.set('ngImport', o.importExpr(R3.core)); + definitionMap.set('type', meta.internalType); + + // Only generate providedIn property if it has a non-null value + if (meta.providedIn !== undefined) { + const providedIn = convertFromProviderExpression(meta.providedIn); + if ((providedIn as o.LiteralExpr).value !== null) { + definitionMap.set('providedIn', providedIn); + } + } + + if (meta.useClass !== undefined) { + definitionMap.set('useClass', convertFromProviderExpression(meta.useClass)); + } + if (meta.useExisting !== undefined) { + definitionMap.set('useExisting', convertFromProviderExpression(meta.useExisting)); + } + if (meta.useValue !== undefined) { + definitionMap.set('useValue', convertFromProviderExpression(meta.useValue)); + } + // Factories do not contain `ForwardRef`s since any types are already wrapped in a function call + // so the types will not be eagerly evaluated. Therefore we do not need to process this expression + // with `convertFromProviderExpression()`. + if (meta.useFactory !== undefined) { + definitionMap.set('useFactory', meta.useFactory); + } + + if (meta.deps !== undefined) { + definitionMap.set('deps', o.literalArr(meta.deps.map(compileDependency))); + } + + return definitionMap; +} + +/** + * Convert an `R3ProviderExpression` to an `Expression`, possibly wrapping its expression in a + * `forwardRef()` call. + * + * If `R3ProviderExpression.isForwardRef` is true then the expression was originally wrapped in a + * `forwardRef()` call to prevent the value from being eagerly evaluated in the code. + * + * Normally, the linker will statically process the code, putting the `expression` inside a factory + * function so the `forwardRef()` wrapper is not evaluated before it has been defined. But if the + * partial declaration is evaluated by the JIT compiler the `forwardRef()` call is still needed to + * prevent eager evaluation of the `expression`. + * + * So in partial declarations, expressions that could be forward-refs are wrapped in `forwardRef()` + * calls, and this is then unwrapped in the linker as necessary. + * + * See `packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts` and + * `packages/compiler/src/jit_compiler_facade.ts` for more information. + */ +function convertFromProviderExpression({expression, isForwardRef}: R3ProviderExpression): + o.Expression { + return isForwardRef ? generateForwardRef(expression) : expression; +} diff --git a/packages/compiler/src/render3/partial/injector.ts b/packages/compiler/src/render3/partial/injector.ts index 5322d2b82a..1f9db81f7b 100644 --- a/packages/compiler/src/render3/partial/injector.ts +++ b/packages/compiler/src/render3/partial/injector.ts @@ -22,6 +22,9 @@ export function compileDeclareInjectorFromMetadata(meta: R3InjectorMetadata): R3 return {expression, type, statements: []}; } +/** + * Gathers the declaration fields for an Injector into a `DefinitionMap`. + */ function createInjectorDefinitionMap(meta: R3InjectorMetadata): DefinitionMap { const definitionMap = new DefinitionMap(); diff --git a/packages/compiler/src/render3/partial/ng_module.ts b/packages/compiler/src/render3/partial/ng_module.ts index f9ad4ed28d..85e3d84a77 100644 --- a/packages/compiler/src/render3/partial/ng_module.ts +++ b/packages/compiler/src/render3/partial/ng_module.ts @@ -23,6 +23,9 @@ export function compileDeclareNgModuleFromMetadata(meta: R3NgModuleMetadata): R3 return {expression, type, statements: []}; } +/** + * Gathers the declaration fields for an NgModule into a `DefinitionMap`. + */ function createNgModuleDefinitionMap(meta: R3NgModuleMetadata): DefinitionMap { const definitionMap = new DefinitionMap(); diff --git a/packages/compiler/src/render3/partial/pipe.ts b/packages/compiler/src/render3/partial/pipe.ts index bea20bcc20..8cad26992f 100644 --- a/packages/compiler/src/render3/partial/pipe.ts +++ b/packages/compiler/src/render3/partial/pipe.ts @@ -26,8 +26,7 @@ export function compileDeclarePipeFromMetadata(meta: R3PipeMetadata): R3Compiled } /** - * Gathers the declaration fields for a Pipe into a `DefinitionMap`. This allows for reusing - * this logic for components, as they extend the Pipe metadata. + * Gathers the declaration fields for a Pipe into a `DefinitionMap`. */ export function createPipeDefinitionMap(meta: R3PipeMetadata): DefinitionMap { diff --git a/packages/compiler/src/render3/partial/util.ts b/packages/compiler/src/render3/partial/util.ts index c99168a979..3be414b160 100644 --- a/packages/compiler/src/render3/partial/util.ts +++ b/packages/compiler/src/render3/partial/util.ts @@ -6,6 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ import * as o from '../../output/output_ast'; +import {R3DependencyMetadata} from '../r3_factory'; +import {Identifiers} from '../r3_identifiers'; +import {DefinitionMap} from '../view/util'; +import {R3DeclareDependencyMetadata} from './api'; /** * Creates an array literal expression from the given array, mapping all values to an expression @@ -46,3 +50,48 @@ export function toOptionalLiteralMap( return null; } } + +export function compileDependencies(deps: R3DependencyMetadata[]|'invalid'|null): o.LiteralExpr| + o.LiteralArrayExpr { + if (deps === 'invalid') { + // The `deps` can be set to the string "invalid" by the `unwrapConstructorDependencies()` + // function, which tries to convert `ConstructorDeps` into `R3DependencyMetadata[]`. + return o.literal('invalid'); + } else if (deps === null) { + return o.literal(null); + } else { + return o.literalArr(deps.map(compileDependency)); + } +} + +export function compileDependency(dep: R3DependencyMetadata): o.LiteralMapExpr { + const depMeta = new DefinitionMap(); + depMeta.set('token', dep.token); + if (dep.attributeNameType !== null) { + depMeta.set('attribute', o.literal(true)); + } + if (dep.host) { + depMeta.set('host', o.literal(true)); + } + if (dep.optional) { + depMeta.set('optional', o.literal(true)); + } + if (dep.self) { + depMeta.set('self', o.literal(true)); + } + if (dep.skipSelf) { + depMeta.set('skipSelf', o.literal(true)); + } + return depMeta.toLiteralMap(); +} + +/** + * Generate an expression that has the given `expr` wrapped in the following form: + * + * ``` + * forwardRef(() => expr) + * ``` + */ +export function generateForwardRef(expr: o.Expression): o.Expression { + return o.importExpr(Identifiers.forwardRef).callFn([o.fn([], [new o.ReturnStatement(expr)])]); +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 89f26c9813..f0636178df 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -229,6 +229,11 @@ export class Identifiers { static forwardRef: o.ExternalReference = {name: 'forwardRef', moduleName: CORE}; static resolveForwardRef: o.ExternalReference = {name: 'resolveForwardRef', moduleName: CORE}; + static ɵɵdefineInjectable: o.ExternalReference = {name: 'ɵɵdefineInjectable', moduleName: CORE}; + static declareInjectable: o.ExternalReference = {name: 'ɵɵngDeclareInjectable', moduleName: CORE}; + static InjectableDeclaration: + o.ExternalReference = {name: 'ɵɵInjectableDeclaration', moduleName: CORE}; + static resolveWindow: o.ExternalReference = {name: 'ɵɵresolveWindow', moduleName: CORE}; static resolveDocument: o.ExternalReference = {name: 'ɵɵresolveDocument', moduleName: CORE}; static resolveBody: o.ExternalReference = {name: 'ɵɵresolveBody', moduleName: CORE}; diff --git a/packages/compiler/test/compiler_facade_interface_spec.ts b/packages/compiler/test/compiler_facade_interface_spec.ts index e0481e504a..69cb4be6ae 100644 --- a/packages/compiler/test/compiler_facade_interface_spec.ts +++ b/packages/compiler/test/compiler_facade_interface_spec.ts @@ -76,6 +76,11 @@ const coreR3InjectableMetadataFacade: core.R3InjectableMetadataFacade = const compilerR3InjectableMetadataFacade: compiler.R3InjectableMetadataFacade = null! as core.R3InjectableMetadataFacade; +const coreR3DeclareInjectableFacade: core.R3DeclareInjectableFacade = + null! as compiler.R3DeclareInjectableFacade; +const compilerR3DeclareInjectableFacade: compiler.R3DeclareInjectableFacade = + null! as core.R3DeclareInjectableFacade; + const coreR3NgModuleMetadataFacade: core.R3NgModuleMetadataFacade = null! as compiler.R3NgModuleMetadataFacade; const compilerR3NgModuleMetadataFacade: compiler.R3NgModuleMetadataFacade = diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 5343e4583c..3bfb7d2fc1 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -33,6 +33,8 @@ export interface CompilerFacade { angularCoreEnv: CoreEnvironment, sourceMapUrl: string, declaration: R3DeclarePipeFacade): any; compileInjectable( angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectableMetadataFacade): any; + compileInjectableDeclaration( + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DeclareInjectableFacade): any; compileInjector( angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3InjectorMetadataFacade): any; compileInjectorDeclaration( @@ -80,7 +82,9 @@ export type StringMapWithRename = { [key: string]: string|[string, string]; }; -export type Provider = any; +export type Provider = unknown; +export type Type = Function; +export type OpaqueValue = unknown; export enum FactoryTarget { Directive = 0, @@ -91,7 +95,7 @@ export enum FactoryTarget { } export interface R3DependencyMetadataFacade { - token: unknown; + token: OpaqueValue; attribute: string|null; host: boolean; optional: boolean; @@ -100,7 +104,7 @@ export interface R3DependencyMetadataFacade { } export interface R3DeclareDependencyMetadataFacade { - token: unknown; + token: OpaqueValue; attribute?: boolean; host?: boolean; optional?: boolean; @@ -110,25 +114,25 @@ export interface R3DeclareDependencyMetadataFacade { export interface R3PipeMetadataFacade { name: string; - type: any; + type: Type; pipeName: string; pure: boolean; } export interface R3InjectableMetadataFacade { name: string; - type: any; + type: Type; typeArgumentCount: number; - providedIn: any; - useClass?: any; - useFactory?: any; - useExisting?: any; - useValue?: any; - userDeps?: R3DependencyMetadataFacade[]; + providedIn?: Type|'root'|'platform'|'any'|null; + useClass?: OpaqueValue; + useFactory?: OpaqueValue; + useExisting?: OpaqueValue; + useValue?: OpaqueValue; + deps?: R3DependencyMetadataFacade[]; } export interface R3NgModuleMetadataFacade { - type: any; + type: Type; bootstrap: Function[]; declarations: Function[]; imports: Function[]; @@ -139,19 +143,19 @@ export interface R3NgModuleMetadataFacade { export interface R3InjectorMetadataFacade { name: string; - type: any; - providers: any[]; - imports: any[]; + type: Type; + providers: Provider[]; + imports: OpaqueValue[]; } export interface R3DirectiveMetadataFacade { name: string; - type: any; + type: Type; typeSourceSpan: ParseSourceSpan; selector: string|null; queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; - propMetadata: {[key: string]: any[]}; + propMetadata: {[key: string]: OpaqueValue[]}; lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; @@ -164,7 +168,7 @@ export interface R3DirectiveMetadataFacade { export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { template: string; preserveWhitespaces: boolean; - animations: any[]|undefined; + animations: OpaqueValue[]|undefined; pipes: Map; directives: R3UsedDirectiveMetadata[]; styles: string[]; @@ -174,11 +178,9 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { changeDetection?: ChangeDetectionStrategy; } -export type OpaqueValue = unknown; - export interface R3DeclareDirectiveFacade { selector?: string; - type: Function; + type: Type; inputs?: {[classPropertyName: string]: string|[string, string]}; outputs?: {[classPropertyName: string]: string}; host?: { @@ -229,18 +231,28 @@ export interface R3UsedDirectiveMetadata { export interface R3FactoryDefMetadataFacade { name: string; - type: any; + type: Type; typeArgumentCount: number; deps: R3DependencyMetadataFacade[]|null; target: FactoryTarget; } export interface R3DeclareFactoryFacade { - type: Function; + type: Type; deps: R3DeclareDependencyMetadataFacade[]|null; target: FactoryTarget; } +export interface R3DeclareInjectableFacade { + type: Type; + providedIn?: Type|'root'|'platform'|'any'|null; + useClass?: OpaqueValue; + useFactory?: OpaqueValue; + useExisting?: OpaqueValue; + useValue?: OpaqueValue; + deps?: R3DeclareDependencyMetadataFacade[]; +} + export enum ViewEncapsulation { Emulated = 0, // Historically the 1 value was for `Native` encapsulation which has been removed as of v11. @@ -253,10 +265,10 @@ export type ChangeDetectionStrategy = number; export interface R3QueryMetadataFacade { propertyName: string; first: boolean; - predicate: any|string[]; + predicate: OpaqueValue|string[]; descendants: boolean; emitDistinctChangesOnly: boolean; - read: any|null; + read: OpaqueValue|null; static: boolean; } @@ -271,13 +283,13 @@ export interface R3DeclareQueryMetadataFacade { } export interface R3DeclareInjectorFacade { - type: Function; + type: Type; imports?: OpaqueValue[]; providers?: OpaqueValue[]; } export interface R3DeclareNgModuleFacade { - type: Function; + type: Type; bootstrap?: OpaqueValue[]|(() => OpaqueValue[]); declarations?: OpaqueValue[]|(() => OpaqueValue[]); imports?: OpaqueValue[]|(() => OpaqueValue[]); @@ -287,7 +299,7 @@ export interface R3DeclareNgModuleFacade { } export interface R3DeclarePipeFacade { - type: Function; + type: Type; name: string; pure?: boolean; } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index bae931b12c..b7aba6b5fa 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -272,6 +272,7 @@ export { ɵɵngDeclareComponent, ɵɵngDeclareDirective, ɵɵngDeclareFactory, + ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe, diff --git a/packages/core/src/di/jit/environment.ts b/packages/core/src/di/jit/environment.ts index c77c0e1f1a..d1402174eb 100644 --- a/packages/core/src/di/jit/environment.ts +++ b/packages/core/src/di/jit/environment.ts @@ -5,6 +5,7 @@ * 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 {resolveForwardRef} from '../forward_ref'; import {ɵɵinject, ɵɵinvalidFactoryDep} from '../injector_compatibility'; import {ɵɵdefineInjectable, ɵɵdefineInjector} from '../interface/defs'; @@ -18,4 +19,5 @@ export const angularCoreDiEnv: {[name: string]: Function} = { 'ɵɵdefineInjector': ɵɵdefineInjector, 'ɵɵinject': ɵɵinject, 'ɵɵinvalidFactoryDep': ɵɵinvalidFactoryDep, + 'resolveForwardRef': resolveForwardRef, }; diff --git a/packages/core/src/di/jit/injectable.ts b/packages/core/src/di/jit/injectable.ts index cec23913d7..f88f5bec1a 100644 --- a/packages/core/src/di/jit/injectable.ts +++ b/packages/core/src/di/jit/injectable.ts @@ -24,7 +24,7 @@ import {convertDependencies, reflectDependencies} from './util'; * Compile an Angular injectable according to its `Injectable` metadata, and patch the resulting * injectable def (`ɵprov`) onto the injectable type. */ -export function compileInjectable(type: Type, srcMeta?: Injectable): void { +export function compileInjectable(type: Type, meta?: Injectable): void { let ngInjectableDef: any = null; let ngFactoryDef: any = null; @@ -34,8 +34,7 @@ export function compileInjectable(type: Type, srcMeta?: Injectable): void { get: () => { if (ngInjectableDef === null) { ngInjectableDef = getCompilerFacade().compileInjectable( - angularCoreDiEnv, `ng:///${type.name}/ɵprov.js`, - getInjectableMetadata(type, srcMeta)); + angularCoreDiEnv, `ng:///${type.name}/ɵprov.js`, getInjectableMetadata(type, meta)); } return ngInjectableDef; }, @@ -47,12 +46,11 @@ export function compileInjectable(type: Type, srcMeta?: Injectable): void { Object.defineProperty(type, NG_FACTORY_DEF, { get: () => { if (ngFactoryDef === null) { - const metadata = getInjectableMetadata(type, srcMeta); const compiler = getCompilerFacade(); ngFactoryDef = compiler.compileFactory(angularCoreDiEnv, `ng:///${type.name}/ɵfac.js`, { - name: metadata.name, - type: metadata.type, - typeArgumentCount: metadata.typeArgumentCount, + name: type.name, + type, + typeArgumentCount: 0, // In JIT mode types are not available nor used. deps: reflectDependencies(type), target: compiler.FactoryTarget.Injectable }); @@ -94,23 +92,19 @@ function getInjectableMetadata(type: Type, srcMeta?: Injectable): R3Injecta type: type, typeArgumentCount: 0, providedIn: meta.providedIn, - userDeps: undefined, }; if ((isUseClassProvider(meta) || isUseFactoryProvider(meta)) && meta.deps !== undefined) { - compilerMeta.userDeps = convertDependencies(meta.deps); + compilerMeta.deps = convertDependencies(meta.deps); } + // Check to see if the user explicitly provided a `useXxxx` property. if (isUseClassProvider(meta)) { - // The user explicitly specified useClass, and may or may not have provided deps. - compilerMeta.useClass = resolveForwardRef(meta.useClass); + compilerMeta.useClass = meta.useClass; } else if (isUseValueProvider(meta)) { - // The user explicitly specified useValue. - compilerMeta.useValue = resolveForwardRef(meta.useValue); + compilerMeta.useValue = meta.useValue; } else if (isUseFactoryProvider(meta)) { - // The user explicitly specified useFactory. compilerMeta.useFactory = meta.useFactory; } else if (isUseExistingProvider(meta)) { - // The user explicitly specified useExisting. - compilerMeta.useExisting = resolveForwardRef(meta.useExisting); + compilerMeta.useExisting = meta.useExisting; } return compilerMeta; } diff --git a/packages/core/src/render3/jit/partial.ts b/packages/core/src/render3/jit/partial.ts index b84d308d5e..840ef31f1b 100644 --- a/packages/core/src/render3/jit/partial.ts +++ b/packages/core/src/render3/jit/partial.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {getCompilerFacade, R3DeclareComponentFacade, R3DeclareDirectiveFacade, R3DeclareFactoryFacade, R3DeclareInjectorFacade, R3DeclareNgModuleFacade, R3DeclarePipeFacade} from '../../compiler/compiler_facade'; +import {getCompilerFacade, R3DeclareComponentFacade, R3DeclareDirectiveFacade, R3DeclareFactoryFacade, R3DeclareInjectableFacade, R3DeclareInjectorFacade, R3DeclareNgModuleFacade, R3DeclarePipeFacade} from '../../compiler/compiler_facade'; import {angularCoreEnv} from './environment'; /** @@ -42,6 +42,17 @@ export function ɵɵngDeclareFactory(decl: R3DeclareFactoryFacade): unknown { angularCoreEnv, `ng:///${decl.type.name}/ɵfac.js`, decl); } +/** + * Compiles a partial injectable declaration object into a full injectable definition object. + * + * @codeGenApi + */ +export function ɵɵngDeclareInjectable(decl: R3DeclareInjectableFacade): unknown { + const compiler = getCompilerFacade(); + return compiler.compileInjectableDeclaration( + angularCoreEnv, `ng:///${decl.type.name}/ɵprov.js`, decl); +} + /** * These enums are used in the partial factory declaration calls. */ diff --git a/packages/core/test/acceptance/providers_spec.ts b/packages/core/test/acceptance/providers_spec.ts index ec0be8cf2e..b2f0f043cc 100644 --- a/packages/core/test/acceptance/providers_spec.ts +++ b/packages/core/test/acceptance/providers_spec.ts @@ -657,8 +657,10 @@ describe('providers', () => { constructor(public foo: SomeProvider) {} } - TestBed.configureTestingModule( - {declarations: [App], providers: [{provide: SomeProvider, useClass: SomeProviderImpl}]}); + // We don't configure the `SomeProvider` in the TestingModule so that it uses the + // tree-shakable provider given in the `@Injectable` decorator above, which makes use of the + // `forwardRef()`. + TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); diff --git a/packages/core/test/render3/jit/declare_injectable_spec.ts b/packages/core/test/render3/jit/declare_injectable_spec.ts new file mode 100644 index 0000000000..f4d0b312a7 --- /dev/null +++ b/packages/core/test/render3/jit/declare_injectable_spec.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {forwardRef, InjectionToken, Injector, ɵcreateInjector, ɵsetCurrentInjector, ɵɵdefineInjector, ɵɵInjectableDeclaration, ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule} from '@angular/core'; + +describe('Injectable declaration jit compilation', () => { + let previousInjector: Injector|null|undefined; + beforeEach(() => previousInjector = ɵsetCurrentInjector(ɵcreateInjector(TestInjector))); + afterEach(() => ɵsetCurrentInjector(previousInjector)); + + it('should compile a minimal injectable declaration that delegates to `ɵfac`', () => { + const provider = Minimal.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('Minimal'); + expect(provider.factory).toBe(Minimal.ɵfac); + const instance = provider.factory(); + expect(instance).toBeInstanceOf(Minimal); + }); + + it('should compile a simple `useClass` injectable declaration', () => { + const provider = UseClass.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('UseClass'); + const instance = provider.factory(); + expect(instance).toBeInstanceOf(UseClass); + }); + + it('should compile a simple `useFactory` injectable declaration', () => { + const provider = UseFactory.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('UseFactory'); + const instance = provider.factory(); + expect(instance).toBeInstanceOf(UseFactory); + expect(instance.msg).toEqual('from factory'); + }); + + it('should compile a simple `useValue` injectable declaration', () => { + const provider = UseValue.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('UseValue'); + const instance = provider.factory(); + expect(instance).toEqual('a value'); + }); + + it('should compile a simple `useExisting` injectable declaration', () => { + const provider = UseExisting.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('UseExisting'); + const instance = provider.factory(); + expect(instance).toEqual('existing'); + }); + + it('should compile a `useClass` injectable declaration with dependencies', () => { + const provider = DependingClass.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('DependingClass'); + const instance = provider.factory(); + expect(instance).toBeInstanceOf(DependingClass); + expect(instance.testClass).toBeInstanceOf(UseClass); + }); + + it('should compile a `useFactory` injectable declaration with dependencies', () => { + const provider = DependingFactory.ɵprov as ɵɵInjectableDeclaration; + expect((provider.token as any).name).toEqual('DependingFactory'); + const instance = provider.factory(); + expect(instance).toBeInstanceOf(DependingFactory); + expect(instance.testClass).toBeInstanceOf(UseClass); + }); + + it('should unwrap a `ForwardRef` `useClass` injectable declaration', () => { + class TestClass { + static ɵprov = ɵɵngDeclareInjectable({ + type: TestClass, + useClass: forwardRef(function() { + return FutureClass; + }) + }); + } + class FutureClass { + static ɵfac = () => new FutureClass(); + } + const provider = TestClass.ɵprov as ɵɵInjectableDeclaration; + const instance = provider.factory(); + expect(instance).toBeInstanceOf(FutureClass); + }); + + it('should unwrap a `ForwardRef` `providedIn` injectable declaration', () => { + const expected = {}; + class TestClass { + static ɵprov = ɵɵngDeclareInjectable({ + type: TestClass, + providedIn: forwardRef(() => FutureModule), + useValue: expected, + }); + } + class FutureModule { + static ɵinj = ɵɵngDeclareInjector({type: FutureModule}); + } + + const injector = ɵcreateInjector(FutureModule); + const actual = injector.get(TestClass); + expect(actual).toBe(expected); + }); +}); + +class Minimal { + static ɵfac = () => new Minimal(); + static ɵprov = ɵɵngDeclareInjectable({type: Minimal}); +} + +class UseClass { + static ɵprov = ɵɵngDeclareInjectable({type: UseClass, useClass: UseClass}); +} + +class UseFactory { + constructor(readonly msg: string) {} + static ɵprov = + ɵɵngDeclareInjectable({type: UseFactory, useFactory: () => new UseFactory('from factory')}); +} + +class UseValue { + constructor(readonly msg: string) {} + static ɵprov = ɵɵngDeclareInjectable({type: UseValue, useValue: 'a value'}); +} + +const UseExistingToken = new InjectionToken('UseExistingToken'); + +class UseExisting { + static ɵprov = ɵɵngDeclareInjectable({type: UseExisting, useExisting: UseExistingToken}); +} + +class DependingClass { + constructor(readonly testClass: UseClass) {} + static ɵprov = ɵɵngDeclareInjectable( + {type: DependingClass, useClass: DependingClass, deps: [{token: UseClass}]}); +} + +class DependingFactory { + constructor(readonly testClass: UseClass) {} + static ɵprov = ɵɵngDeclareInjectable({ + type: DependingFactory, + useFactory: (dep: UseClass) => new DependingFactory(dep), + deps: [{token: UseClass}] + }); +} + +class TestInjector { + static ɵinj = ɵɵdefineInjector({ + providers: [ + UseClass, + UseFactory, + UseValue, + UseExisting, + DependingClass, + {provide: UseExistingToken, useValue: 'existing'}, + ], + }); +} diff --git a/packages/core/test/render3/jit_environment_spec.ts b/packages/core/test/render3/jit_environment_spec.ts index e3c12a83eb..dba42a6362 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([ 'ɵɵComponentDeclaration', 'ɵɵDirectiveDeclaration', + 'ɵɵInjectableDeclaration', 'ɵɵInjectorDeclaration', 'ɵɵInjectorDef', 'ɵɵNgModuleDeclaration', @@ -30,6 +31,7 @@ const PARTIAL_ONLY = new Set([ 'ɵɵngDeclareDirective', 'ɵɵngDeclareComponent', 'ɵɵngDeclareFactory', + 'ɵɵngDeclareInjectable', 'ɵɵngDeclareInjector', 'ɵɵngDeclareNgModule', 'ɵɵngDeclarePipe',