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',