diff --git a/packages/common/src/pipes/async_pipe.ts b/packages/common/src/pipes/async_pipe.ts index 6ff3db2cd8..40d565d2f1 100644 --- a/packages/common/src/pipes/async_pipe.ts +++ b/packages/common/src/pipes/async_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, EventEmitter, Injectable, OnDestroy, Pipe, PipeTransform, WrappedValue, ɵisObservable, ɵisPromise, ɵlooseIdentical} from '@angular/core'; +import {ChangeDetectorRef, EventEmitter, OnDestroy, Pipe, PipeTransform, WrappedValue, ɵisObservable, ɵisPromise, ɵlooseIdentical} from '@angular/core'; import {Observable, SubscriptionLike} from 'rxjs'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; @@ -67,7 +67,6 @@ const _observableStrategy = new ObservableStrategy(); * * @publicApi */ -@Injectable() @Pipe({name: 'async', pure: false}) export class AsyncPipe implements OnDestroy, PipeTransform { private _latestValue: any = null; diff --git a/packages/common/src/pipes/case_conversion_pipes.ts b/packages/common/src/pipes/case_conversion_pipes.ts index 39d3b7af39..3f60d852e5 100644 --- a/packages/common/src/pipes/case_conversion_pipes.ts +++ b/packages/common/src/pipes/case_conversion_pipes.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; /** @@ -24,7 +24,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; * @ngModule CommonModule * @publicApi */ -@Injectable() @Pipe({name: 'lowercase'}) export class LowerCasePipe implements PipeTransform { /** @@ -68,7 +67,6 @@ const unicodeWordMatch = * @ngModule CommonModule * @publicApi */ -@Injectable() @Pipe({name: 'titlecase'}) export class TitleCasePipe implements PipeTransform { /** @@ -93,7 +91,6 @@ export class TitleCasePipe implements PipeTransform { * @ngModule CommonModule * @publicApi */ -@Injectable() @Pipe({name: 'uppercase'}) export class UpperCasePipe implements PipeTransform { /** diff --git a/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts index de5a1810eb..cf573de219 100644 --- a/packages/common/src/pipes/date_pipe.ts +++ b/packages/common/src/pipes/date_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; import {formatDate} from '../i18n/format_date'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; @@ -150,7 +150,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; * @publicApi */ // clang-format on -@Injectable() @Pipe({name: 'date', pure: true}) export class DatePipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private locale: string) {} diff --git a/packages/common/src/pipes/i18n_plural_pipe.ts b/packages/common/src/pipes/i18n_plural_pipe.ts index 30e9bd865b..0a4fa6a030 100644 --- a/packages/common/src/pipes/i18n_plural_pipe.ts +++ b/packages/common/src/pipes/i18n_plural_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {NgLocalization, getPluralCategory} from '../i18n/localization'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; @@ -26,7 +26,6 @@ const _INTERPOLATION_REGEXP: RegExp = /#/g; * * @publicApi */ -@Injectable() @Pipe({name: 'i18nPlural', pure: true}) export class I18nPluralPipe implements PipeTransform { constructor(private _localization: NgLocalization) {} diff --git a/packages/common/src/pipes/i18n_select_pipe.ts b/packages/common/src/pipes/i18n_select_pipe.ts index 97325282a6..f6cfa58a55 100644 --- a/packages/common/src/pipes/i18n_select_pipe.ts +++ b/packages/common/src/pipes/i18n_select_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; /** @@ -26,7 +26,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; * * @publicApi */ -@Injectable() @Pipe({name: 'i18nSelect', pure: true}) export class I18nSelectPipe implements PipeTransform { /** diff --git a/packages/common/src/pipes/json_pipe.ts b/packages/common/src/pipes/json_pipe.ts index 148fad51b1..7ecbdddbe3 100644 --- a/packages/common/src/pipes/json_pipe.ts +++ b/packages/common/src/pipes/json_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; /** * @ngModule CommonModule @@ -23,7 +23,6 @@ import {Injectable, Pipe, PipeTransform} from '@angular/core'; * * @publicApi */ -@Injectable() @Pipe({name: 'json', pure: false}) export class JsonPipe implements PipeTransform { /** diff --git a/packages/common/src/pipes/keyvalue_pipe.ts b/packages/common/src/pipes/keyvalue_pipe.ts index f9304e9c67..5770f6a75b 100644 --- a/packages/common/src/pipes/keyvalue_pipe.ts +++ b/packages/common/src/pipes/keyvalue_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Pipe, PipeTransform} from '@angular/core'; +import {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Pipe, PipeTransform} from '@angular/core'; function makeKeyValuePair(key: K, value: V): KeyValue { return {key: key, value: value}; @@ -43,7 +43,6 @@ export interface KeyValue { * * @publicApi */ -@Injectable() @Pipe({name: 'keyvalue', pure: false}) export class KeyValuePipe implements PipeTransform { constructor(private readonly differs: KeyValueDiffers) {} diff --git a/packages/common/src/pipes/number_pipe.ts b/packages/common/src/pipes/number_pipe.ts index 8e5401023f..8f00995e73 100644 --- a/packages/common/src/pipes/number_pipe.ts +++ b/packages/common/src/pipes/number_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; import {formatCurrency, formatNumber, formatPercent} from '../i18n/format_number'; import {getCurrencySymbol} from '../i18n/locale_data_api'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; @@ -46,7 +46,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; * * @publicApi */ -@Injectable() @Pipe({name: 'number'}) export class DecimalPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} @@ -100,7 +99,6 @@ export class DecimalPipe implements PipeTransform { * * @publicApi */ -@Injectable() @Pipe({name: 'percent'}) export class PercentPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} @@ -155,7 +153,6 @@ export class PercentPipe implements PipeTransform { * * @publicApi */ -@Injectable() @Pipe({name: 'currency'}) export class CurrencyPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} diff --git a/packages/common/src/pipes/slice_pipe.ts b/packages/common/src/pipes/slice_pipe.ts index bf591c7868..13f57fc2ae 100644 --- a/packages/common/src/pipes/slice_pipe.ts +++ b/packages/common/src/pipes/slice_pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Pipe, PipeTransform} from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; /** @@ -44,7 +44,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; * * @publicApi */ -@Injectable() @Pipe({name: 'slice', pure: false}) export class SlicePipe implements PipeTransform { /** diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 71bf20d46c..13bf8ba0e5 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -171,8 +171,12 @@ export class DecorationAnalyzer { for (const {handler, analysis} of clazz.matches) { const result = handler.compile(clazz.declaration, analysis, constantPool); if (Array.isArray(result)) { - compilations.push(...result); - } else { + result.forEach(current => { + if (!compilations.some(compilation => compilation.name === current.name)) { + compilations.push(current); + } + }); + } else if (!compilations.some(compilation => compilation.name === result.name)) { compilations.push(result); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 72ad3d4893..374390f6e5 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; +import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; @@ -486,7 +486,7 @@ export class ComponentDecoratorHandler implements CompileResult[] { const meta = analysis.meta; const res = compileComponentFromMetadata(meta, pool, makeBindingParser()); - const factoryRes = compileNgFactoryDefField(meta); + const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject}); if (analysis.metadataStmt !== null) { factoryRes.statements.push(analysis.metadataStmt); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 394514a7c5..6c6f47f928 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler'; +import {ConstantPool, EMPTY_SOURCE_SPAN, Expression, Identifiers, ParseError, ParsedHostBindings, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings, verifyHostBindings} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -94,7 +94,7 @@ export class DirectiveDecoratorHandler implements CompileResult[] { const meta = analysis.meta; const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser()); - const factoryRes = compileNgFactoryDefField(meta); + const factoryRes = compileNgFactoryDefField({...meta, injectFn: Identifiers.directiveInject}); if (analysis.metadataStmt !== null) { factoryRes.statements.push(analysis.metadataStmt); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 31edc42660..08cd1ded95 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 {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; +import {Expression, Identifiers, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -14,12 +14,15 @@ import {DefaultImportRecorder} from '../../imports'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; +import {compileNgFactoryDefField} from './factory'; import {generateSetClassMetadataCall} from './metadata'; -import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, validateConstructorDependencies} from './util'; +import {findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, unwrapForwardRef, validateConstructorDependencies} from './util'; export interface InjectableHandlerData { meta: R3InjectableMetadata; metadataStmt: Statement|null; + ctorDeps: R3DependencyMetadata[]|'invalid'|null; + needsFactory: boolean; } /** @@ -49,41 +52,65 @@ export class InjectableDecoratorHandler implements } analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput { + const meta = extractInjectableMetadata(node, decorator, this.reflector); + const decorators = this.reflector.getDecoratorsOfDeclaration(node); + return { analysis: { - meta: extractInjectableMetadata( - node, decorator, this.reflector, this.defaultImportRecorder, this.isCore, + meta, + ctorDeps: extractInjectableCtorDeps( + node, meta, decorator, this.reflector, this.defaultImportRecorder, this.isCore, this.strictCtorDeps), metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore), + // Avoid generating multiple factories if a class has + // more Angular decorators, apart from Injectable. + needsFactory: !decorators || + decorators.every(current => !isAngularCore(current) || current.name === 'Injectable') }, }; } - compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult { + compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult[] { const res = compileIvyInjectable(analysis.meta); const statements = res.statements; - if (analysis.metadataStmt !== null) { - statements.push(analysis.metadataStmt); + const results: CompileResult[] = []; + + if (analysis.needsFactory) { + const meta = analysis.meta; + const factoryRes = compileNgFactoryDefField({ + name: meta.name, + type: meta.type, + typeArgumentCount: meta.typeArgumentCount, + deps: analysis.ctorDeps, + injectFn: Identifiers.inject + }); + if (analysis.metadataStmt !== null) { + factoryRes.statements.push(analysis.metadataStmt); + } + results.push(factoryRes); } - return { + + results.push({ name: 'ngInjectableDef', initializer: res.expression, statements, type: res.type, - }; + }); + + return results; } } /** - * Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the input + * Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the + * input * metadata needed to run `compileIvyInjectable`. * * A `null` return value indicates this is @Injectable has invalid data. */ function extractInjectableMetadata( - clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, - defaultImportRecorder: DefaultImportRecorder, isCore: boolean, - strictCtorDeps: boolean): R3InjectableMetadata { + clazz: ClassDeclaration, decorator: Decorator, + reflector: ReflectionHost): R3InjectableMetadata { const name = clazz.name.text; const type = new WrappedNodeExpr(clazz.name); const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0; @@ -92,53 +119,13 @@ function extractInjectableMetadata( ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called'); } if (decorator.args.length === 0) { - // Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be - // subject to the same validation. However, existing Angular code abuses @Injectable, applying - // it to things like abstract classes with constructors that were never meant for use with - // Angular's DI. - // - // To deal with this, @Injectable() without an argument is more lenient, and if the constructor - // signature does not work for DI then an ngInjectableDef that throws. - let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null; - if (strictCtorDeps) { - ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); - } else { - const possibleCtorDeps = - getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); - if (possibleCtorDeps !== null) { - if (possibleCtorDeps.deps !== null) { - // This use of @Injectable has valid constructor dependencies. - ctorDeps = possibleCtorDeps.deps; - } else { - // This use of @Injectable is technically invalid. Generate a factory function which - // throws - // an error. - // TODO(alxhub): log warnings for the bad use of @Injectable. - ctorDeps = 'invalid'; - } - } - } return { name, type, typeArgumentCount, - providedIn: new LiteralExpr(null), ctorDeps, + providedIn: new LiteralExpr(null), }; } else if (decorator.args.length === 1) { - const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); - let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null; - - // rawCtorDeps will be null if the class has no constructor. - if (rawCtorDeps !== null) { - if (rawCtorDeps.deps !== null) { - // A constructor existed and had valid dependencies. - ctorDeps = rawCtorDeps.deps; - } else { - // A constructor existed but had invalid dependencies. - ctorDeps = 'invalid'; - } - } - const metaNode = decorator.args[0]; // Firstly make sure the decorator argument is an inline literal - if not, it's illegal to // transport references from one location to another. This is the problem that lowering @@ -170,27 +157,25 @@ function extractInjectableMetadata( name, type, typeArgumentCount, - ctorDeps, providedIn, - useValue: new WrappedNodeExpr(meta.get('useValue') !), + useValue: new WrappedNodeExpr(unwrapForwardRef(meta.get('useValue') !, reflector)), }; } else if (meta.has('useExisting')) { return { name, type, typeArgumentCount, - ctorDeps, providedIn, - useExisting: new WrappedNodeExpr(meta.get('useExisting') !), + useExisting: new WrappedNodeExpr(unwrapForwardRef(meta.get('useExisting') !, reflector)), }; } else if (meta.has('useClass')) { return { name, type, typeArgumentCount, - ctorDeps, providedIn, - useClass: new WrappedNodeExpr(meta.get('useClass') !), userDeps, + useClass: new WrappedNodeExpr(unwrapForwardRef(meta.get('useClass') !, reflector)), + userDeps, }; } else if (meta.has('useFactory')) { // useFactory is special - the 'deps' property must be analyzed. @@ -200,14 +185,10 @@ function extractInjectableMetadata( type, typeArgumentCount, providedIn, - useFactory: factory, ctorDeps, userDeps, + useFactory: factory, userDeps, }; } else { - if (strictCtorDeps) { - // Since use* was not provided, validate the deps according to strictCtorDeps. - validateConstructorDependencies(clazz, rawCtorDeps); - } - return {name, type, typeArgumentCount, providedIn, ctorDeps}; + return {name, type, typeArgumentCount, providedIn}; } } else { throw new FatalDiagnosticError( @@ -215,7 +196,69 @@ function extractInjectableMetadata( } } +function extractInjectableCtorDeps( + clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator, + reflector: ReflectionHost, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, + strictCtorDeps: boolean) { + if (decorator.args === null) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called'); + } + let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null; + + if (decorator.args.length === 0) { + // Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be + // subject to the same validation. However, existing Angular code abuses @Injectable, applying + // it to things like abstract classes with constructors that were never meant for use with + // Angular's DI. + // + // To deal with this, @Injectable() without an argument is more lenient, and if the + // constructor + // signature does not work for DI then an ngInjectableDef that throws. + if (strictCtorDeps) { + ctorDeps = getValidConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); + } else { + const possibleCtorDeps = + getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); + if (possibleCtorDeps !== null) { + if (possibleCtorDeps.deps !== null) { + // This use of @Injectable has valid constructor dependencies. + ctorDeps = possibleCtorDeps.deps; + } else { + // This use of @Injectable is technically invalid. Generate a factory function which + // throws + // an error. + // TODO(alxhub): log warnings for the bad use of @Injectable. + ctorDeps = 'invalid'; + } + } + } + + return ctorDeps; + } else if (decorator.args.length === 1) { + const rawCtorDeps = getConstructorDependencies(clazz, reflector, defaultImportRecorder, isCore); + + // rawCtorDeps will be null if the class has no constructor. + if (rawCtorDeps !== null) { + if (rawCtorDeps.deps !== null) { + // A constructor existed and had valid dependencies. + ctorDeps = rawCtorDeps.deps; + } else { + // A constructor existed but had invalid dependencies. + ctorDeps = 'invalid'; + } + } + + if (strictCtorDeps && !meta.useValue && !meta.useExisting && !meta.useClass && + !meta.useFactory) { + // Since use* was not provided, validate the deps according to strictCtorDeps. + validateConstructorDependencies(clazz, rawCtorDeps); + } + } + + return ctorDeps; +} function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata { const meta: R3DependencyMetadata = { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 4c44aafca8..4bbfbab24f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler'; +import {Identifiers, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -109,7 +109,11 @@ export class PipeDecoratorHandler implements DecoratorHandler([ ['ɵɵdefineNgModule', 'ɵɵdefineNgModule'], ['ɵɵsetNgModuleScope', 'ɵɵsetNgModuleScope'], ['ɵɵinject', 'ɵɵinject'], + ['ɵɵFactoryDef', 'ɵɵFactoryDef'], ['ɵsetClassMetadata', 'setClassMetadata'], ['ɵɵInjectableDef', 'ɵɵInjectableDef'], ['ɵɵInjectorDef', 'ɵɵInjectorDef'], diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index da694ea6d3..381e5a2c48 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -353,10 +353,14 @@ export class IvyCompilation { const compileMatchRes = match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool); this.perf.stop(compileSpan); - if (!Array.isArray(compileMatchRes)) { + if (Array.isArray(compileMatchRes)) { + compileMatchRes.forEach(result => { + if (!res.some(r => r.name === result.name)) { + res.push(result); + } + }); + } else if (!res.some(result => result.name === compileMatchRes.name)) { res.push(compileMatchRes); - } else { - res.push(...compileMatchRes); } } diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_di_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_di_spec.ts index 76c6e5bc2c..02b9de690a 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_di_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_di_spec.ts @@ -66,4 +66,294 @@ describe('compiler compliance: dependency injection', () => { expectEmit(result.source, factory, 'Incorrect factory'); }); + it('should create a factory definition for an injectable', () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable} from '@angular/core'; + + class MyDependency {} + + @Injectable() + export class MyService { + constructor(dep: MyDependency) {} + } + ` + } + }; + + const factory = ` + MyService.ngFactoryDef = function MyService_Factory(t) { + return new (t || MyService)($r3$.ɵɵinject(MyDependency)); + }`; + + const def = ` + MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: MyService, + factory: function(t) { + return MyService.ngFactoryDef(t); + }, + providedIn: null + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, factory, 'Incorrect factory definition'); + expectEmit(result.source, def, 'Incorrect injectable definition'); + }); + + it('should create a single ngFactoryDef if the class has more than one decorator', () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable, Pipe} from '@angular/core'; + + @Injectable() + @Pipe({name: 'my-pipe'}) + export class MyPipe { + } + ` + } + }; + + const result = compile(files, angularFiles).source; + const matches = result.match(/MyPipe\.ngFactoryDef = function MyPipe_Factory/g); + expect(matches ? matches.length : 0).toBe(1); + }); + + it('should delegate directly to the alternate factory when setting `useFactory` without `deps`', + () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable} from '@angular/core'; + + class MyAlternateService {} + + function alternateFactory() { + return new MyAlternateService(); + } + + @Injectable({ + useFactory: alternateFactory + }) + export class MyService { + } + ` + } + }; + + const def = ` + MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: MyService, + factory: function() { + return alternateFactory(); + }, + providedIn: null + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, def, 'Incorrect injectable definition'); + }); + + it('should not delegate directly to the alternate factory when setting `useFactory` with `deps`', + () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable} from '@angular/core'; + + class SomeDep {} + class MyAlternateService {} + + @Injectable({ + useFactory: () => new MyAlternateFactory(), + deps: [SomeDep] + }) + export class MyService { + } + ` + } + }; + + const def = ` + MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: MyService, + factory: function MyService_Factory(t) { + var r = null; + if (t) { + (r = new t()); + } else { + (r = (() => new MyAlternateFactory())($r3$.ɵɵinject(SomeDep))); + } + return r; + }, + providedIn: null + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, def, 'Incorrect injectable definition'); + }); + + it('should delegate directly to the alternate class factory when setting `useClass` without `deps`', + () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable} from '@angular/core'; + + @Injectable() + class MyAlternateService {} + + @Injectable({ + useClass: MyAlternateService + }) + export class MyService { + } + ` + } + }; + + const factory = ` + MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: MyService, + factory: function(t) { + return MyAlternateService.ngFactoryDef(t); + }, + providedIn: null + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, factory, 'Incorrect factory definition'); + }); + + it('should not delegate directly to the alternate class when setting `useClass` with `deps`', + () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable} from '@angular/core'; + + class SomeDep {} + + @Injectable() + class MyAlternateService {} + + @Injectable({ + useClass: MyAlternateService, + deps: [SomeDep] + }) + export class MyService { + } + ` + } + }; + + const factory = ` + MyService.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: MyService, + factory: function MyService_Factory(t) { + var r = null; + if (t) { + (r = new t()); + } else { + (r = new MyAlternateService($r3$.ɵɵinject(SomeDep))); + } + return r; + }, + providedIn: null + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, factory, 'Incorrect factory definition'); + }); + + it('should unwrap forward refs when delegating to a different class', () => { + const files = { + app: { + 'spec.ts': ` + import {Injectable, forwardRef} from '@angular/core'; + + @Injectable({providedIn: 'root', useClass: forwardRef(() => SomeProviderImpl)}) + abstract class SomeProvider { + } + + @Injectable() + class SomeProviderImpl extends SomeProvider { + } + ` + } + }; + + const factory = ` + SomeProvider.ngInjectableDef = $r3$.ɵɵdefineInjectable({ + token: SomeProvider, + factory: function(t) { + return SomeProviderImpl.ngFactoryDef(t); + }, + providedIn: 'root' + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, factory, 'Incorrect factory definition'); + }); + + it('should have the pipe factory take precedence over the injectable factory, if a class has multiple decorators', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, Pipe, PipeTransform, Injectable} from '@angular/core'; + + @Injectable() + class Service {} + + @Injectable() + @Pipe({name: 'myPipe'}) + export class MyPipe implements PipeTransform { + constructor(service: Service) {} + transform(value: any, ...args: any[]) { return value; } + } + + @Pipe({name: 'myOtherPipe'}) + @Injectable() + export class MyOtherPipe implements PipeTransform { + constructor(service: Service) {} + transform(value: any, ...args: any[]) { return value; } + } + + @Component({ + selector: 'my-app', + template: '{{0 | myPipe | myOtherPipe}}' + }) + export class MyApp {} + + @NgModule({declarations: [MyPipe, MyOtherPipe, MyApp], declarations: [Service]}) + export class MyModule {} + ` + } + }; + + const result = compile(files, angularFiles); + const source = result.source; + + const MyPipeFactory = ` + MyPipe.ngFactoryDef = function MyPipe_Factory(t) { return new (t || MyPipe)($r3$.ɵɵdirectiveInject(Service)); }; + `; + + const MyOtherPipeFactory = ` + MyOtherPipe.ngFactoryDef = function MyOtherPipe_Factory(t) { return new (t || MyOtherPipe)($r3$.ɵɵdirectiveInject(Service)); }; + `; + + expectEmit(source, MyPipeFactory, 'Invalid pipe factory function'); + expectEmit(source, MyOtherPipeFactory, 'Invalid pipe factory function'); + expect(source.match(/MyPipe\.ngFactoryDef =/g) !.length).toBe(1); + expect(source.match(/MyOtherPipe\.ngFactoryDef =/g) !.length).toBe(1); + }); + }); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index c94c751234..f55217066d 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -67,6 +67,8 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with a generic service', () => { @@ -83,6 +85,7 @@ runInEachFileSystem(os => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Store.ngInjectableDef ='); const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef>;'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef>;'); }); @@ -106,11 +109,15 @@ runInEachFileSystem(os => { expect(jsContents).toContain('Dep.ngInjectableDef ='); expect(jsContents).toContain('Service.ngInjectableDef ='); expect(jsContents) - .toContain('return new (t || Service)(i0.ɵɵinject(Dep)); }, providedIn: \'root\' });'); + .toContain( + 'Service.ngFactoryDef = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };'); + expect(jsContents).toContain('providedIn: \'root\' })'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with providedIn and factory without errors', () => { @@ -128,13 +135,14 @@ runInEachFileSystem(os => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Service.ngInjectableDef ='); - expect(jsContents).toContain('(r = new t());'); - expect(jsContents).toContain('(r = (function () { return new Service(); })());'); - expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); - expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); + expect(jsContents) + .toContain('factory: function () { return (function () { return new Service(); })(); }'); + expect(jsContents).toContain('Service_Factory(t) { return new (t || Service)(); }'); + expect(jsContents).toContain(', providedIn: \'root\' });'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with providedIn and factory with deps without errors', () => { @@ -156,13 +164,14 @@ runInEachFileSystem(os => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Service.ngInjectableDef ='); expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); - expect(jsContents).toContain('(r = new t(i0.ɵɵinject(Dep)));'); + expect(jsContents).toContain('return new (t || Service)(i0.ɵɵinject(Dep));'); expect(jsContents) .toContain('(r = (function (dep) { return new Service(dep); })(i0.ɵɵinject(Dep)));'); expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngFactoryDef: i0.ɵɵFactoryDef;'); }); it('should compile @Injectable with an @Optional dependency', () => { @@ -1282,7 +1291,7 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toMatch(/if \(t\).*throw new Error.* else .* '42'/ms); + expect(jsContents).toMatch(/function Test_Factory\(t\) { throw new Error\(/ms); }); }); @@ -1290,33 +1299,35 @@ runInEachFileSystem(os => { it('should compile an @Injectable on a class with a non-injectable constructor', () => { env.tsconfig({strictInjectionParameters: false}); env.write('test.ts', ` - import {Injectable} from '@angular/core'; + import {Injectable} from '@angular/core'; - @Injectable() - export class Test { - constructor(private notInjectable: string) {} - } - `); + @Injectable() + export class Test { + constructor(private notInjectable: string) {} + } + `); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); + expect(jsContents) + .toContain('Test.ngFactoryDef = function Test_Factory(t) { throw new Error('); }); it('should compile an @Injectable provided in the root on a class with a non-injectable constructor', () => { env.tsconfig({strictInjectionParameters: false}); env.write('test.ts', ` - import {Injectable} from '@angular/core'; - @Injectable({providedIn: 'root'}) - export class Test { - constructor(private notInjectable: string) {} - } - `); + import {Injectable} from '@angular/core'; + @Injectable({providedIn: 'root'}) + export class Test { + constructor(private notInjectable: string) {} + } + `); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error('); + expect(jsContents) + .toContain('Test.ngFactoryDef = function Test_Factory(t) { throw new Error('); }); }); diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index b95c8c2115..b30398b9e5 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -40,9 +40,7 @@ export interface CompilerFacade { compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3BaseMetadataFacade): any; compileFactory( - angularCoreEnv: CoreEnvironment, sourceMapUrl: string, - meta: R3PipeMetadataFacade|R3DirectiveMetadataFacade|R3ComponentMetadataFacade, - isPipe?: boolean): any; + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any; createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan; @@ -94,7 +92,6 @@ export interface R3InjectableMetadataFacade { name: string; type: any; typeArgumentCount: number; - ctorDeps: R3DependencyMetadataFacade[]|null; providedIn: any; useClass?: any; useFactory?: any; @@ -164,6 +161,15 @@ export interface R3BaseMetadataFacade { viewQueries?: R3QueryMetadataFacade[]; } +export interface R3FactoryDefMetadataFacade { + name: string; + type: any; + typeArgumentCount: number; + deps: R3DependencyMetadataFacade[]|null; + injectFn: 'directiveInject'|'inject'; + isPipe: boolean; +} + export type ViewEncapsulation = number; export type ChangeDetectionStrategy = number; diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index dd2b88f603..6bf33b76ad 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -63,6 +63,7 @@ export class Identifiers { }; static inject: o.ExternalReference = {name: 'ɵɵinject', moduleName: CORE}; + 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}; diff --git a/packages/compiler/src/injectable_compiler_2.ts b/packages/compiler/src/injectable_compiler_2.ts index 5520b56ed1..cb9bab774a 100644 --- a/packages/compiler/src/injectable_compiler_2.ts +++ b/packages/compiler/src/injectable_compiler_2.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectFlags} from './core'; import {Identifiers} from './identifiers'; import * as o from './output/output_ast'; -import {R3DependencyMetadata, R3FactoryDelegateType, R3FactoryMetadata, compileFactoryFunction} from './render3/r3_factory'; +import {R3DependencyMetadata, R3FactoryDelegateType, compileFactoryFunction} from './render3/r3_factory'; import {mapToMapExpression, typeWithParameters} from './render3/util'; export interface InjectableDef { @@ -22,7 +21,6 @@ export interface R3InjectableMetadata { name: string; type: o.Expression; typeArgumentCount: number; - ctorDeps: R3DependencyMetadata[]|'invalid'|null; providedIn: o.Expression; useClass?: o.Expression; useFactory?: o.Expression; @@ -38,7 +36,7 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { name: meta.name, type: meta.type, typeArgumentCount: meta.typeArgumentCount, - deps: meta.ctorDeps, + deps: [], injectFn: Identifiers.inject, }; @@ -67,19 +65,22 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { } else if (useClassOnSelf) { result = compileFactoryFunction(factoryMeta); } else { - result = compileFactoryFunction({ - ...factoryMeta, - delegate: meta.useClass, - delegateType: R3FactoryDelegateType.Factory, - }); + result = delegateToFactory(meta.useClass); } } else if (meta.useFactory !== undefined) { - result = compileFactoryFunction({ - ...factoryMeta, - delegate: meta.useFactory, - delegateDeps: meta.userDeps || [], - delegateType: R3FactoryDelegateType.Function, - }); + if (meta.userDeps !== undefined) { + result = compileFactoryFunction({ + ...factoryMeta, + delegate: meta.useFactory, + delegateDeps: meta.userDeps || [], + delegateType: R3FactoryDelegateType.Function, + }); + } else { + result = { + statements: [], + factory: o.fn([], [new o.ReturnStatement(meta.useFactory.callFn([]))]) + }; + } } else if (meta.useValue !== undefined) { // Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for // client code because meta.useValue is an Expression which will be defined even if the actual @@ -95,7 +96,7 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { expression: o.importExpr(Identifiers.inject).callFn([meta.useExisting]), }); } else { - result = compileFactoryFunction(factoryMeta); + result = delegateToFactory(meta.type); } const token = meta.type; @@ -112,3 +113,12 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { statements: result.statements, }; } + +function delegateToFactory(type: o.Expression) { + return { + statements: [], + // () => meta.type.ngFactoryDef(t) + factory: o.fn([new o.FnParam('t', o.DYNAMIC_TYPE)], [new o.ReturnStatement(type.callMethod( + 'ngFactoryDef', [o.variable('t')]))]) + }; +} diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 9b20d292bd..8be8159abd 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -7,9 +7,10 @@ */ -import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3BaseMetadataFacade, R3ComponentMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface'; +import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3BaseMetadataFacade, R3ComponentMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface'; import {ConstantPool} from './constant_pool'; import {HostBinding, HostListener, Input, Output, Type} from './core'; +import {Identifiers} from './identifiers'; import {compileInjectable} from './injectable_compiler_2'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config'; import {DeclareVarStmt, Expression, LiteralExpr, Statement, StmtModifier, WrappedNodeExpr} from './output/output_ast'; @@ -59,7 +60,6 @@ export class CompilerFacadeImpl implements CompilerFacade { useFactory: wrapExpression(facade, USE_FACTORY), useValue: wrapExpression(facade, USE_VALUE), useExisting: wrapExpression(facade, USE_EXISTING), - ctorDeps: convertR3DependencyMetadataArray(facade.ctorDeps), userDeps: convertR3DependencyMetadataArray(facade.userDeps) || undefined, }); @@ -154,14 +154,15 @@ export class CompilerFacadeImpl implements CompilerFacade { } compileFactory( - angularCoreEnv: CoreEnvironment, sourceMapUrl: string, - meta: R3PipeMetadataFacade|R3DirectiveMetadataFacade|R3ComponentMetadataFacade, - isPipe = false) { + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade) { const factoryRes = compileFactoryFromMetadata({ name: meta.name, type: new WrappedNodeExpr(meta.type), typeArgumentCount: meta.typeArgumentCount, - deps: convertR3DependencyMetadataArray(meta.deps), isPipe + deps: convertR3DependencyMetadataArray(meta.deps), + injectFn: meta.injectFn === 'directiveInject' ? Identifiers.directiveInject : + Identifiers.inject, + isPipe: meta.isPipe }); return this.jitExpression( factoryRes.factory, angularCoreEnv, sourceMapUrl, factoryRes.statements); diff --git a/packages/compiler/src/render3/r3_factory.ts b/packages/compiler/src/render3/r3_factory.ts index d23dced86c..5c8a7eee04 100644 --- a/packages/compiler/src/render3/r3_factory.ts +++ b/packages/compiler/src/render3/r3_factory.ts @@ -86,7 +86,8 @@ export interface R3FactoryDefMetadata { name: string; type: o.Expression; typeArgumentCount: number; - deps: R3DependencyMetadata[]|null; + deps: R3DependencyMetadata[]|'invalid'|null; + injectFn: o.ExternalReference; isPipe?: boolean; } @@ -267,8 +268,7 @@ export function compileFactoryFromMetadata(meta: R3FactoryDefMetadata): R3Factor type: meta.type, deps: meta.deps, typeArgumentCount: meta.typeArgumentCount, - // TODO(crisbeto): this should be refactored once we start using it for injectables. - injectFn: R3.directiveInject, + injectFn: meta.injectFn, }, meta.isPipe); } diff --git a/packages/compiler/src/render3/r3_pipe_compiler.ts b/packages/compiler/src/render3/r3_pipe_compiler.ts index e13416a486..275d22ee19 100644 --- a/packages/compiler/src/render3/r3_pipe_compiler.ts +++ b/packages/compiler/src/render3/r3_pipe_compiler.ts @@ -89,7 +89,8 @@ export function compilePipeFromRender2( pure: pipe.pure, }; const res = compilePipeFromMetadata(metadata); - const factoryRes = compileFactoryFromMetadata({...metadata, isPipe: true}); + const factoryRes = + compileFactoryFromMetadata({...metadata, injectFn: R3.directiveInject, isPipe: true}); const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Pipe); const ngFactoryDefStatement = new o.ClassStmt( /* name */ name, diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 4f0c5ca0e3..8f27dd2944 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -325,7 +325,7 @@ export function compileDirectiveFromRender2( const meta = directiveMetadataFromGlobalMetadata(directive, outputCtx, reflector); const res = compileDirectiveFromMetadata(meta, outputCtx.constantPool, bindingParser); - const factoryRes = compileFactoryFromMetadata(meta); + const factoryRes = compileFactoryFromMetadata({...meta, injectFn: R3.directiveInject}); const ngFactoryDefStatement = new o.ClassStmt( name, null, [new o.ClassField( @@ -378,7 +378,7 @@ export function compileComponentFromRender2( i18nUseExternalIds: true, }; const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser); - const factoryRes = compileFactoryFromMetadata(meta); + const factoryRes = compileFactoryFromMetadata({...meta, injectFn: R3.directiveInject}); const ngFactoryDefStatement = new o.ClassStmt( name, null, [new o.ClassField( diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index b95c8c2115..b30398b9e5 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -40,9 +40,7 @@ export interface CompilerFacade { compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3BaseMetadataFacade): any; compileFactory( - angularCoreEnv: CoreEnvironment, sourceMapUrl: string, - meta: R3PipeMetadataFacade|R3DirectiveMetadataFacade|R3ComponentMetadataFacade, - isPipe?: boolean): any; + angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any; createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan; @@ -94,7 +92,6 @@ export interface R3InjectableMetadataFacade { name: string; type: any; typeArgumentCount: number; - ctorDeps: R3DependencyMetadataFacade[]|null; providedIn: any; useClass?: any; useFactory?: any; @@ -164,6 +161,15 @@ export interface R3BaseMetadataFacade { viewQueries?: R3QueryMetadataFacade[]; } +export interface R3FactoryDefMetadataFacade { + name: string; + type: any; + typeArgumentCount: number; + deps: R3DependencyMetadataFacade[]|null; + injectFn: 'directiveInject'|'inject'; + isPipe: boolean; +} + export type ViewEncapsulation = number; export type ChangeDetectionStrategy = number; diff --git a/packages/core/src/di/jit/injectable.ts b/packages/core/src/di/jit/injectable.ts index bd0e2e4bca..fdb67b76c6 100644 --- a/packages/core/src/di/jit/injectable.ts +++ b/packages/core/src/di/jit/injectable.ts @@ -8,7 +8,9 @@ import {R3InjectableMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade'; import {Type} from '../../interface/type'; +import {NG_FACTORY_DEF} from '../../render3/fields'; import {getClosureSafeProperty} from '../../util/property'; +import {resolveForwardRef} from '../forward_ref'; import {Injectable} from '../injectable'; import {NG_INJECTABLE_DEF} from '../interface/defs'; import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, ValueProvider, ValueSansProvider} from '../interface/provider'; @@ -23,59 +25,45 @@ import {convertDependencies, reflectDependencies} from './util'; * `ngInjectableDef` onto the injectable type. */ export function compileInjectable(type: Type, srcMeta?: Injectable): void { - let def: any = null; + let ngInjectableDef: any = null; + let ngFactoryDef: any = null; // if NG_INJECTABLE_DEF is already defined on this class then don't overwrite it - if (type.hasOwnProperty(NG_INJECTABLE_DEF)) return; - - Object.defineProperty(type, NG_INJECTABLE_DEF, { - get: () => { - if (def === null) { - // Allow the compilation of a class with a `@Injectable()` decorator without parameters - const meta: Injectable = srcMeta || {providedIn: null}; - const hasAProvider = isUseClassProvider(meta) || isUseFactoryProvider(meta) || - isUseValueProvider(meta) || isUseExistingProvider(meta); - - - const compilerMeta: R3InjectableMetadataFacade = { - name: type.name, - type: type, - typeArgumentCount: 0, - providedIn: meta.providedIn, - ctorDeps: reflectDependencies(type), - userDeps: undefined, - }; - if ((isUseClassProvider(meta) || isUseFactoryProvider(meta)) && meta.deps !== undefined) { - compilerMeta.userDeps = convertDependencies(meta.deps); + if (!type.hasOwnProperty(NG_INJECTABLE_DEF)) { + Object.defineProperty(type, NG_INJECTABLE_DEF, { + get: () => { + if (ngInjectableDef === null) { + ngInjectableDef = getCompilerFacade().compileInjectable( + angularCoreDiEnv, `ng:///${type.name}/ngInjectableDef.js`, + getInjectableMetadata(type, srcMeta)); } - if (!hasAProvider) { - // In the case the user specifies a type provider, treat it as {provide: X, useClass: X}. - // The deps will have been reflected above, causing the factory to create the class by - // calling - // its constructor with injected deps. - compilerMeta.useClass = type; - } else if (isUseClassProvider(meta)) { - // The user explicitly specified useClass, and may or may not have provided deps. - compilerMeta.useClass = meta.useClass; - } else if (isUseValueProvider(meta)) { - // The user explicitly specified 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 = meta.useExisting; - } else { - // Can't happen - either hasAProvider will be false, or one of the providers will be set. - throw new Error(`Unreachable state.`); + return ngInjectableDef; + }, + }); + } + + // if NG_FACTORY_DEF is already defined on this class then don't overwrite it + if (!type.hasOwnProperty(NG_FACTORY_DEF)) { + Object.defineProperty(type, NG_FACTORY_DEF, { + get: () => { + if (ngFactoryDef === null) { + const metadata = getInjectableMetadata(type, srcMeta); + ngFactoryDef = getCompilerFacade().compileFactory( + angularCoreDiEnv, `ng:///${type.name}/ngFactoryDef.js`, { + name: metadata.name, + type: metadata.type, + typeArgumentCount: metadata.typeArgumentCount, + deps: reflectDependencies(type), + injectFn: 'inject', + isPipe: false + }); } - def = getCompilerFacade().compileInjectable( - angularCoreDiEnv, `ng:///${type.name}/ngInjectableDef.js`, compilerMeta); - } - return def; - }, - }); + return ngFactoryDef; + }, + // Leave this configurable so that the factories from directives or pipes can take precedence. + configurable: true + }); + } } type UseClassProvider = Injectable & ClassSansProvider & {deps?: any[]}; @@ -98,3 +86,32 @@ function isUseFactoryProvider(meta: Injectable): meta is Injectable&FactorySansP function isUseExistingProvider(meta: Injectable): meta is Injectable&ExistingSansProvider { return (meta as ExistingSansProvider).useExisting !== undefined; } + +function getInjectableMetadata(type: Type, srcMeta?: Injectable): R3InjectableMetadataFacade { + // Allow the compilation of a class with a `@Injectable()` decorator without parameters + const meta: Injectable = srcMeta || {providedIn: null}; + const compilerMeta: R3InjectableMetadataFacade = { + name: type.name, + type: type, + typeArgumentCount: 0, + providedIn: meta.providedIn, + userDeps: undefined, + }; + if ((isUseClassProvider(meta) || isUseFactoryProvider(meta)) && meta.deps !== undefined) { + compilerMeta.userDeps = convertDependencies(meta.deps); + } + if (isUseClassProvider(meta)) { + // The user explicitly specified useClass, and may or may not have provided deps. + compilerMeta.useClass = resolveForwardRef(meta.useClass); + } else if (isUseValueProvider(meta)) { + // The user explicitly specified useValue. + compilerMeta.useValue = resolveForwardRef(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); + } + return compilerMeta; +} diff --git a/packages/core/src/r3_symbols.ts b/packages/core/src/r3_symbols.ts index 6f8a80bbdb..66771f54db 100644 --- a/packages/core/src/r3_symbols.ts +++ b/packages/core/src/r3_symbols.ts @@ -25,6 +25,7 @@ export {ɵɵinject} from './di/injector_compatibility'; export {ɵɵInjectableDef, ɵɵInjectorDef, ɵɵdefineInjectable, ɵɵdefineInjector} from './di/interface/defs'; export {NgModuleDef, ɵɵNgModuleDefWithMeta} from './metadata/ng_module'; export {ɵɵdefineNgModule} from './render3/definition'; +export {ɵɵFactoryDef} from './render3/interfaces/definition'; export {setClassMetadata} from './render3/metadata'; export {NgModuleFactory} from './render3/ng_module_ref'; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 2b4b1aeff7..f07c050da6 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -753,11 +753,11 @@ export function getBaseDef(type: any): ɵɵBaseDef|null { export function getFactoryDef(type: any, throwNotFound: true): FactoryFn; export function getFactoryDef(type: any): FactoryFn|null; export function getFactoryDef(type: any, throwNotFound?: boolean): FactoryFn|null { - const factoryFn = type[NG_FACTORY_DEF] || null; - if (!factoryFn && throwNotFound === true && ngDevMode) { + const hasFactoryDef = type.hasOwnProperty(NG_FACTORY_DEF); + if (!hasFactoryDef && throwNotFound === true && ngDevMode) { throw new Error(`Type ${stringify(type)} does not have 'ngFactoryDef' property.`); } - return factoryFn; + return hasFactoryDef ? type[NG_FACTORY_DEF] : null; } export function getNgModuleDef(type: any, throwNotFound: true): NgModuleDef; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 0fd81994ea..d49f92b73a 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -16,7 +16,7 @@ import {Type} from '../interface/type'; import {assertDefined, assertEqual} from '../util/assert'; import {getFactoryDef} from './definition'; -import {NG_ELEMENT_ID} from './fields'; +import {NG_ELEMENT_ID, NG_FACTORY_DEF} from './fields'; import {DirectiveDef, FactoryFn} from './interfaces/definition'; import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags, TNODE, isFactory} from './interfaces/injector'; import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node'; @@ -642,9 +642,11 @@ export function ɵɵgetFactoryOf(type: Type): FactoryFn|null { }) as any; } - // TODO(crisbeto): unify injectable factories with getFactory. - const def = getInjectableDef(typeAny) || getInjectorDef(typeAny); - const factory = def && def.factory || getFactoryDef(typeAny); + let factory = getFactoryDef(typeAny); + if (factory === null) { + const injectorDef = getInjectorDef(typeAny); + factory = injectorDef && injectorDef.factory; + } return factory || null; } @@ -653,7 +655,7 @@ export function ɵɵgetFactoryOf(type: Type): FactoryFn|null { */ export function ɵɵgetInheritedFactory(type: Type): (type: Type) => T { const proto = Object.getPrototypeOf(type.prototype).constructor as Type; - const factory = ɵɵgetFactoryOf(proto); + const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf(proto); if (factory !== null) { return factory; } else { diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 4d8b380c99..2c846dff8e 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -7,9 +7,8 @@ */ import {R3DirectiveMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade'; -import {CompilerFacade, R3BaseMetadataFacade, R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface'; +import {R3BaseMetadataFacade, R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface'; import {resolveForwardRef} from '../../di/forward_ref'; -import {compileInjectable} from '../../di/jit/injectable'; import {getReflect, reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; import {Query} from '../../metadata/di'; @@ -43,31 +42,52 @@ export function compileComponent(type: Type, metadata: Component): void { (typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode(); let ngComponentDef: any = null; - let ngFactoryDef: any = null; // Metadata may have resources which need to be resolved. maybeQueueResolutionOfComponentResources(type, metadata); - Object.defineProperty(type, NG_FACTORY_DEF, { - get: () => { - if (ngFactoryDef === null) { - const compiler = getCompilerFacade(); - const meta = getComponentMetadata(compiler, type, metadata); - ngFactoryDef = compiler.compileFactory( - angularCoreEnv, `ng:///${type.name}/ngFactory.js`, meta.metadata); - } - return ngFactoryDef; - }, - // Make the property configurable in dev mode to allow overriding in tests - configurable: !!ngDevMode, - }); + // Note that we're using the same function as `Directive`, because that's only subset of metadata + // that we need to create the ngFactoryDef. We're avoiding using the component metadata + // because we'd have to resolve the asynchronous templates. + addDirectiveFactoryDef(type, metadata); Object.defineProperty(type, NG_COMPONENT_DEF, { get: () => { if (ngComponentDef === null) { const compiler = getCompilerFacade(); - const meta = getComponentMetadata(compiler, type, metadata); - ngComponentDef = compiler.compileComponent(angularCoreEnv, meta.templateUrl, meta.metadata); + + if (componentNeedsResolution(metadata)) { + const error = [`Component '${type.name}' is not resolved:`]; + if (metadata.templateUrl) { + error.push(` - templateUrl: ${metadata.templateUrl}`); + } + if (metadata.styleUrls && metadata.styleUrls.length) { + error.push(` - styleUrls: ${JSON.stringify(metadata.styleUrls)}`); + } + error.push(`Did you run and wait for 'resolveComponentResources()'?`); + throw new Error(error.join('\n')); + } + + const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`; + const meta: R3ComponentMetadataFacade = { + ...directiveMetadata(type, metadata), + typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl), + template: metadata.template || '', + preserveWhitespaces: metadata.preserveWhitespaces || false, + styles: metadata.styles || EMPTY_ARRAY, + animations: metadata.animations, + directives: [], + changeDetection: metadata.changeDetection, + pipes: new Map(), + encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, + interpolation: metadata.interpolation, + viewProviders: metadata.viewProviders || null, + }; + if (meta.usesInheritance) { + addBaseDefToUndecoratedParents(type); + } + + ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta); // When NgModule decorator executed, we enqueued the module definition such that // it would only dequeue and add itself as module scope to all of its declarations, @@ -90,45 +110,6 @@ export function compileComponent(type: Type, metadata: Component): void { // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); - - // Add ngInjectableDef so components are reachable through the module injector by default - // This is mostly to support injecting components in tests. In real application code, - // components should be retrieved through the node injector, so this isn't a problem. - compileInjectable(type); -} - -function getComponentMetadata(compiler: CompilerFacade, type: Type, metadata: Component) { - if (componentNeedsResolution(metadata)) { - const error = [`Component '${type.name}' is not resolved:`]; - if (metadata.templateUrl) { - error.push(` - templateUrl: ${metadata.templateUrl}`); - } - if (metadata.styleUrls && metadata.styleUrls.length) { - error.push(` - styleUrls: ${JSON.stringify(metadata.styleUrls)}`); - } - error.push(`Did you run and wait for 'resolveComponentResources()'?`); - throw new Error(error.join('\n')); - } - - const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`; - const meta: R3ComponentMetadataFacade = { - ...directiveMetadata(type, metadata), - typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl), - template: metadata.template || '', - preserveWhitespaces: metadata.preserveWhitespaces || false, - styles: metadata.styles || EMPTY_ARRAY, - animations: metadata.animations, - directives: [], - changeDetection: metadata.changeDetection, - pipes: new Map(), - encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, - interpolation: metadata.interpolation, - viewProviders: metadata.viewProviders || null, - }; - if (meta.usesInheritance) { - addBaseDefToUndecoratedParents(type); - } - return {metadata: meta, templateUrl}; } function hasSelectorScope(component: Type): component is Type& @@ -145,23 +126,8 @@ function hasSelectorScope(component: Type): component is Type& */ export function compileDirective(type: Type, directive: Directive | null): void { let ngDirectiveDef: any = null; - let ngFactoryDef: any = null; - Object.defineProperty(type, NG_FACTORY_DEF, { - get: () => { - if (ngFactoryDef === null) { - // `directive` can be null in the case of abstract directives as a base class - // that use `@Directive()` with no selector. In that case, pass empty object to the - // `directiveMetadata` function instead of null. - const meta = getDirectiveMetadata(type, directive || {}); - ngFactoryDef = getCompilerFacade().compileFactory( - angularCoreEnv, `ng:///${type.name}/ngFactory.js`, meta.metadata); - } - return ngFactoryDef; - }, - // Make the property configurable in dev mode to allow overriding in tests - configurable: !!ngDevMode, - }); + addDirectiveFactoryDef(type, directive || {}); Object.defineProperty(type, NG_DIRECTIVE_DEF, { get: () => { @@ -178,11 +144,6 @@ export function compileDirective(type: Type, directive: Directive | null): // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); - - // Add ngInjectableDef so directives are reachable through the module injector by default - // This is mostly to support injecting directives in tests. In real application code, - // directives should be retrieved through the node injector, so this isn't a problem. - compileInjectable(type); } function getDirectiveMetadata(type: Type, metadata: Directive) { @@ -197,6 +158,24 @@ function getDirectiveMetadata(type: Type, metadata: Directive) { return {metadata: facade, sourceMapUrl}; } +function addDirectiveFactoryDef(type: Type, metadata: Directive | Component) { + let ngFactoryDef: any = null; + + Object.defineProperty(type, NG_FACTORY_DEF, { + get: () => { + if (ngFactoryDef === null) { + const meta = getDirectiveMetadata(type, metadata); + ngFactoryDef = getCompilerFacade().compileFactory( + angularCoreEnv, `ng:///${type.name}/ngFactoryDef.js`, + {...meta.metadata, injectFn: 'directiveInject', isPipe: false}); + } + return ngFactoryDef; + }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode, + }); +} + export function extendsDirectlyFromObject(type: Type): boolean { return Object.getPrototypeOf(type.prototype) === Object.prototype; } @@ -225,7 +204,7 @@ export function directiveMetadata(type: Type, metadata: Directive): R3Direc usesInheritance: !extendsDirectlyFromObject(type), exportAs: extractExportAs(metadata.exportAs), providers: metadata.providers || null, - viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery), + viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery) }; } diff --git a/packages/core/src/render3/jit/pipe.ts b/packages/core/src/render3/jit/pipe.ts index bc08f1f50c..fd17afe889 100644 --- a/packages/core/src/render3/jit/pipe.ts +++ b/packages/core/src/render3/jit/pipe.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {getCompilerFacade} from '../../compiler/compiler_facade'; +import {R3PipeMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade'; import {reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; import {Pipe} from '../../metadata/directives'; @@ -23,7 +23,8 @@ export function compilePipe(type: Type, meta: Pipe): void { if (ngFactoryDef === null) { const metadata = getPipeMetadata(type, meta); ngFactoryDef = getCompilerFacade().compileFactory( - angularCoreEnv, `ng:///${metadata.name}/ngFactory.js`, metadata, true); + angularCoreEnv, `ng:///${metadata.name}/ngFactoryDef.js`, + {...metadata, injectFn: 'directiveInject', isPipe: true}); } return ngFactoryDef; }, @@ -45,7 +46,7 @@ export function compilePipe(type: Type, meta: Pipe): void { }); } -function getPipeMetadata(type: Type, meta: Pipe) { +function getPipeMetadata(type: Type, meta: Pipe): R3PipeMetadataFacade { return { type: type, typeArgumentCount: 0, diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 48fdc21415..b2316cbffc 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -951,6 +951,37 @@ describe('di', () => { `This will become an error in v10. Please add @Injectable() to the "SubSubClass" class.`); } }); + + it('should instantiate correct class when undecorated class extends an injectable', () => { + @Injectable() + class MyService { + id = 1; + } + + class MyRootService extends MyService { + id = 2; + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App], providers: [MyRootService]}); + const warnSpy = spyOn(console, 'warn'); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const provider = TestBed.inject(MyRootService); + + expect(provider instanceof MyRootService).toBe(true); + expect(provider.id).toBe(2); + + if (ivyEnabled) { + expect(warnSpy).toHaveBeenCalledWith( + `DEPRECATED: DI is instantiating a token "MyRootService" that inherits its @Injectable decorator but does not provide one itself.\n` + + `This will become an error in v10. Please add @Injectable() to the "MyRootService" class.`); + } + }); }); describe('inject', () => {