From 290ea57a9309ed3bd9a5c3635d13f7a45945abbf Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Tue, 27 Oct 2020 16:07:08 -0700 Subject: [PATCH] fix(core): Access injected parent values using SelfSkip (#39464) In ViewEngine, SelfSkip would navigate up the tree to get tokens from the parent node, skipping the child. This restores that functionality in Ivy. In ViewEngine, if a special token (e.g. ElementRef) was not found in the NodeInjector tree, the ModuleInjector was also used to lookup that token. While special tokens like ElementRef make sense only in a context of a NodeInjector, we preserved ViewEngine logic for now to avoid breaking changes. We identified 4 scenarios related to @SkipSelf and special tokens where ViewEngine behavior was incorrect and is likely due to bugs. In Ivy this is implemented to provide a more intuitive API. The list of scenarios can be found below. 1. When Injector is used in combination with @Host and @SkipSelf on the first Component within a module and the injector is defined in the module, ViewEngine will get the injector from the module. In Ivy, it does not do this and throws instead. 2. When retrieving a @ViewContainerRef while @SkipSelf and @Host are present, in ViewEngine, it throws an exception. In Ivy it returns the host ViewContainerRef. 3. When retrieving a @ViewContainerRef on an embedded view and @SkipSelf is present, in ViewEngine, the ref is null. In Ivy it returns the parent ViewContainerRef. 4. When utilizing viewProviders and providers, a child component that is nested within a parent component that has @SkipSelf on a viewProvider value, if that provider is provided by the parent component's viewProviders and providers, ViewEngine will return that parent's viewProviders value, which violates how viewProviders' visibility should work. In Ivy, it retrieves the value from providers, as it should. These discrepancies all behave as they should in Ivy and are likely bugs in ViewEngine. PR Close #39464 --- .../size-tracking/integration-payloads.json | 8 +- packages/core/src/render3/di.ts | 114 +- packages/core/src/render3/state.ts | 97 +- packages/core/test/acceptance/di_spec.ts | 1026 ++++++++++++++++- .../cyclic_import/bundle.golden_symbols.json | 6 + .../bundling/forms/bundle.golden_symbols.json | 9 + .../hello_world/bundle.golden_symbols.json | 6 + .../router/bundle.golden_symbols.json | 9 + .../bundling/todo/bundle.golden_symbols.json | 9 + 9 files changed, 1205 insertions(+), 79 deletions(-) diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index b46bef621a..7adccac568 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 17210, + "main-es2015": 17597, "polyfills-es2015": 36709 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 146698, + "main-es2015": 147252, "polyfills-es2015": 36964 } } @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 136062, + "main-es2015": 136703, "polyfills-es2015": 37641 } } @@ -66,4 +66,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 54085071a8..5e0fc857df 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -27,7 +27,7 @@ import {AttributeMarker, TContainerNode, TDirectiveHostNode, TElementContainerNo import {isComponentDef, isComponentHost} from './interfaces/type_checks'; import {DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, INJECTOR, LView, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; import {assertTNodeType} from './node_assert'; -import {enterDI, leaveDI} from './state'; +import {enterDI, getCurrentTNode, getLView, leaveDI} from './state'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {stringifyForError} from './util/misc_utils'; @@ -345,6 +345,51 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str } +function notFoundValueOrThrow( + notFoundValue: T|null, token: Type|InjectionToken, flags: InjectFlags): T|null { + if (flags & InjectFlags.Optional) { + return notFoundValue; + } else { + throwProviderNotFoundError(token, 'NodeInjector'); + } +} + +/** + * Returns the value associated to the given token from the ModuleInjector or throws exception + * + * @param lView The `LView` that contains the `tNode` + * @param token The token to look for + * @param flags Injection flags + * @param notFoundValue The value to return when the injection flags is `InjectFlags.Optional` + * @returns the value from the injector or throws an exception + */ +function lookupTokenUsingModuleInjector( + lView: LView, token: Type|InjectionToken, flags: InjectFlags, notFoundValue?: any): T| + null { + if (flags & InjectFlags.Optional && notFoundValue === undefined) { + // This must be set or the NullInjector will throw for optional deps + notFoundValue = null; + } + + if ((flags & (InjectFlags.Self | InjectFlags.Host)) === 0) { + const moduleInjector = lView[INJECTOR]; + // switch to `injectInjectorOnly` implementation for module injector, since module injector + // should not have access to Component/Directive DI scope (that may happen through + // `directiveInject` implementation) + const previousInjectImplementation = setInjectImplementation(undefined); + try { + if (moduleInjector) { + return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional); + } else { + return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional); + } + } finally { + setInjectImplementation(previousInjectImplementation); + } + } + return notFoundValueOrThrow(notFoundValue, token, flags); +} + /** * Returns the value associated to the given token from the NodeInjectors => ModuleInjector. * @@ -352,8 +397,7 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str * the module injector tree. * * This function patches `token` with `__NG_ELEMENT_ID__` which contains the id for the bloom - * filter. Negative values are reserved for special objects. - * - `-1` is reserved for injecting `Injector` (implemented by `NodeInjector`) + * filter. `-1` is reserved for injecting `Injector` (implemented by `NodeInjector`) * * @param tNode The Node where the search for the injector should start * @param lView The `LView` that contains the `tNode` @@ -370,7 +414,10 @@ export function getOrCreateInjectable( // If the ID stored here is a function, this is a special object like ElementRef or TemplateRef // so just call the factory function to create it. if (typeof bloomHash === 'function') { - enterDI(lView, tNode); + if (!enterDI(lView, tNode, flags)) { + // Failed to enter DI use module injector instead. + return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + } try { const value = bloomHash(); if (value == null && !(flags & InjectFlags.Optional)) { @@ -381,10 +428,30 @@ export function getOrCreateInjectable( } finally { leaveDI(); } - } else if (typeof bloomHash == 'number') { + } else if (typeof bloomHash === 'number') { + // This is a value used to identify __NG_ELEMENT_ID__ + // `-1` is a special value used to identify `Injector` types in NodeInjector + // This is a workaround for the fact that if the `Injector.__NG_ELEMENT_ID__` + // would have a factory function (such as `ElementRef`) it would cause Ivy + // to be pulled into the ViewEngine, because they both share `Injector` type. + // This should be refactored to follow `ElementRef` pattern once ViewEngine is + // removed if (bloomHash === -1) { - // `-1` is a special value used to identify `Injector` types. - return new NodeInjector(tNode, lView) as any; + if (!enterDI(lView, tNode, flags)) { + // Failed to enter DI, try module injector instead. If a token is injected with the @Host + // flag, the module injector is not searched for that token in Ivy. + return (flags & InjectFlags.Host) ? + notFoundValueOrThrow(notFoundValue, token, flags) : + lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + } + try { + // Retrieving current `TNode` and `LView` from the state (rather than using `tNode` and + // `lView`), because entering DI (by calling `enterDI`) may cause these values to change + // (in case `@SkipSelf` flag is present). + return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any; + } finally { + leaveDI(); + } } // If the token has a bloom hash, then it is a token which could be in NodeInjector. @@ -453,32 +520,7 @@ export function getOrCreateInjectable( } } - if (flags & InjectFlags.Optional && notFoundValue === undefined) { - // This must be set or the NullInjector will throw for optional deps - notFoundValue = null; - } - - if ((flags & (InjectFlags.Self | InjectFlags.Host)) === 0) { - const moduleInjector = lView[INJECTOR]; - // switch to `injectInjectorOnly` implementation for module injector, since module injector - // should not have access to Component/Directive DI scope (that may happen through - // `directiveInject` implementation) - const previousInjectImplementation = setInjectImplementation(undefined); - try { - if (moduleInjector) { - return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional); - } else { - return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional); - } - } finally { - setInjectImplementation(previousInjectImplementation); - } - } - if (flags & InjectFlags.Optional) { - return notFoundValue; - } else { - throwProviderNotFoundError(token, 'NodeInjector'); - } + return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); } const NOT_FOUND = {}; @@ -582,7 +624,11 @@ export function getNodeInjectable( factory.resolving = true; const previousInjectImplementation = factory.injectImpl ? setInjectImplementation(factory.injectImpl) : null; - enterDI(lView, tNode); + const success = enterDI(lView, tNode, InjectFlags.Default); + ngDevMode && + assertEqual( + success, true, + 'Because flags do not contain \`SkipSelf\' we expect this to always succeed.'); try { value = lView[index] = factory.factory(undefined, tData, lView, tNode); // This code path is hit for both directives and providers. diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index d4e971dcf6..0217e2f60d 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {InjectFlags} from '../di/interface/injector'; import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertLessThan, assertNotEqual} from '../util/assert'; -import {assertLViewOrUndefined, assertTNodeForTView} from './assert'; +import {assertLViewOrUndefined, assertTNodeForLView, assertTNodeForTView} from './assert'; import {DirectiveDef} from './interfaces/definition'; import {TNode, TNodeType} from './interfaces/node'; -import {CONTEXT, DECLARATION_VIEW, HEADER_OFFSET, LView, OpaqueViewState, TData, TVIEW, TView} from './interfaces/view'; +import {CONTEXT, DECLARATION_VIEW, HEADER_OFFSET, LView, OpaqueViewState, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces'; -import {assertTNodeType} from './node_assert'; import {getTNode} from './util/view_utils'; @@ -434,16 +434,89 @@ export function setCurrentQueryIndex(value: number): void { } /** - * This is a light weight version of the `enterView` which is needed by the DI system. - * @param newView - * @param tNode + * Returns a `TNode` of the location where the current `LView` is declared at. + * + * @param lView an `LView` that we want to find parent `TNode` for. */ -export function enterDI(newView: LView, tNode: TNode) { - ngDevMode && assertLViewOrUndefined(newView); - const newLFrame = allocLFrame(); - instructionState.lFrame = newLFrame; - newLFrame.currentTNode = tNode!; - newLFrame.lView = newView; +function getDeclarationTNode(lView: LView): TNode|null { + const tView = lView[TVIEW]; + + // Return the declaration parent for embedded views + if (tView.type === TViewType.Embedded) { + ngDevMode && assertDefined(tView.declTNode, 'Embedded TNodes should have declaration parents.'); + return tView.declTNode; + } + + // Components don't have `TView.declTNode` because each instance of component could be + // inserted in different location, hence `TView.declTNode` is meaningless. + // Falling back to `T_HOST` in case we cross component boundary. + if (tView.type === TViewType.Component) { + return lView[T_HOST]; + } + + // Remaining TNode type is `TViewType.Root` which doesn't have a parent TNode. + return null; +} + +/** + * This is a light weight version of the `enterView` which is needed by the DI system. + * + * @param lView `LView` location of the DI context. + * @param tNode `TNode` for DI context + * @param flags DI context flags. if `SkipSelf` flag is set than we walk up the declaration + * tree from `tNode` until we find parent declared `TElementNode`. + * @returns `true` if we have successfully entered DI associated with `tNode` (or with declared + * `TNode` if `flags` has `SkipSelf`). Failing to enter DI implies that no associated + * `NodeInjector` can be found and we should instead use `ModuleInjector`. + * - If `true` than this call must be fallowed by `leaveDI` + * - If `false` than this call failed and we should NOT call `leaveDI` + */ +export function enterDI(lView: LView, tNode: TNode, flags: InjectFlags) { + ngDevMode && assertLViewOrUndefined(lView); + + if (flags & InjectFlags.SkipSelf) { + ngDevMode && assertTNodeForTView(tNode, lView[TVIEW]); + + let parentTNode = tNode as TNode | null; + let parentLView = lView; + + while (true) { + ngDevMode && assertDefined(parentTNode, 'Parent TNode should be defined'); + parentTNode = parentTNode!.parent as TNode | null; + if (parentTNode === null && !(flags & InjectFlags.Host)) { + parentTNode = getDeclarationTNode(parentLView); + if (parentTNode === null) break; + + // In this case, a parent exists and is definitely an element. So it will definitely + // have an existing lView as the declaration view, which is why we can assume it's defined. + ngDevMode && assertDefined(parentLView, 'Parent LView should be defined'); + parentLView = parentLView[DECLARATION_VIEW]!; + + // In Ivy there are Comment nodes that correspond to ngIf and NgFor embedded directives + // We want to skip those and look only at Elements and ElementContainers to ensure + // we're looking at true parent nodes, and not content or other types. + if (parentTNode.type & (TNodeType.Element | TNodeType.ElementContainer)) { + break; + } + } else { + break; + } + } + if (parentTNode === null) { + // If we failed to find a parent TNode this means that we should use module injector. + return false; + } else { + tNode = parentTNode; + lView = parentLView; + } + } + + ngDevMode && assertTNodeForLView(tNode, lView); + const lFrame = instructionState.lFrame = allocLFrame(); + lFrame.currentTNode = tNode; + lFrame.lView = lView; + + return true; } /** diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 768fbf5827..820416d60e 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,10 +7,11 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core'; +import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ContentChild, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core'; import {ɵINJECTOR_SCOPE} from '@angular/core/src/core'; import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; import {BehaviorSubject} from 'rxjs'; @@ -700,30 +701,6 @@ describe('di', () => { }); }); - it('should skip the current node with @SkipSelf', () => { - @Directive({selector: '[dirA]'}) - class DirectiveA { - constructor(@SkipSelf() public dirB: DirectiveB) {} - } - - @Component({selector: 'my-comp', template: '
'}) - class MyComp { - @ViewChild(DirectiveA) dirA!: DirectiveA; - } - - @Component({template: ''}) - class MyApp { - @ViewChild(MyComp) myComp!: MyComp; - } - - TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); - const fixture = TestBed.createComponent(MyApp); - fixture.detectChanges(); - - const dirA = fixture.componentInstance.myComp.dirA; - expect(dirA.dirB.value).toEqual('parent'); - }); - onlyInIvy('Ivy has different error message when dependency is not found') .it('should check only the current node with @Self', () => { @Directive({selector: '[dirA]'}) @@ -739,6 +716,994 @@ describe('di', () => { .toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/); }); + describe('SkipSelf', () => { + describe('Injectors', () => { + it('should support @SkipSelf when injecting Injectors', () => { + @Component({ + selector: 'parent', + template: '', + providers: [{ + provide: 'token', + useValue: 'PARENT', + }] + }) + class ParentComponent { + } + + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class ChildComponent { + constructor(public injector: Injector, @SkipSelf() public parentInjector: Injector) {} + } + + TestBed.configureTestingModule({ + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + const childComponent = + fixture.debugElement.query(By.directive(ChildComponent)).componentInstance; + expect(childComponent.injector.get('token')).toBe('CHILD'); + expect(childComponent.parentInjector.get('token')).toBe('PARENT'); + }); + + it('should lookup module injector in case @SkipSelf is used and no suitable Injector found in element injector tree', + () => { + let componentInjector: Injector; + let moduleInjector: Injector; + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class MyComponent { + constructor(@SkipSelf() public injector: Injector) { + componentInjector = injector; + } + } + + @NgModule({ + declarations: [MyComponent], + providers: [{ + provide: 'token', + useValue: 'NG_MODULE', + }] + }) + class MyModule { + constructor(public injector: Injector) { + moduleInjector = injector; + } + } + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + + expect(componentInjector!.get('token')).toBe('NG_MODULE'); + expect(moduleInjector!.get('token')).toBe('NG_MODULE'); + }); + + it('should respect @Host in case @SkipSelf is used and no suitable Injector found in element injector tree', + () => { + let componentInjector: Injector; + let moduleInjector: Injector; + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class MyComponent { + constructor(@Host() @SkipSelf() public injector: Injector) { + componentInjector = injector; + } + } + + @NgModule({ + declarations: [MyComponent], + providers: [{ + provide: 'token', + useValue: 'NG_MODULE', + }] + }) + class MyModule { + constructor(public injector: Injector) { + moduleInjector = injector; + } + } + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + + // If a token is injected with the @Host flag, the module injector is not searched + // for that token in Ivy. + if (ivyEnabled) { + expect(() => TestBed.createComponent(MyComponent)) + .toThrowError(/NG0201: No provider for Injector found in NodeInjector/); + } else { + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + + expect(componentInjector!.get('token')).toBe('NG_MODULE'); + expect(moduleInjector!.get('token')).toBe('NG_MODULE'); + } + }); + + it('should throw when injecting Injectors using @SkipSelf and @Host and no Injectors are available in a current view', + () => { + @Component({ + selector: 'parent', + template: '', + providers: [{ + provide: 'token', + useValue: 'PARENT', + }] + }) + class ParentComponent { + } + + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class ChildComponent { + constructor(@Host() @SkipSelf() public injector: Injector) {} + } + + TestBed.configureTestingModule({ + declarations: [ParentComponent, ChildComponent], + }); + + // Ivy has different error message when dependency is not found + const expectedErrorMessage = ivyEnabled ? + /NG0201: No provider for Injector found in NodeInjector/ : + /No provider for Injector/; + expect(() => TestBed.createComponent(ParentComponent)) + .toThrowError(expectedErrorMessage); + }); + + it('should not throw when injecting Injectors using @SkipSelf, @Host, and @Optional and no Injectors are available in a current view', + () => { + @Component({ + selector: 'parent', + template: '', + providers: [{ + provide: 'token', + useValue: 'PARENT', + }] + }) + class ParentComponent { + } + + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class ChildComponent { + constructor(@Host() @SkipSelf() @Optional() public injector: Injector) {} + } + + TestBed.configureTestingModule({ + declarations: [ParentComponent, ChildComponent], + }); + + // Ivy has different error message when dependency is not found + const expectedErrorMessage = ivyEnabled ? + /NG0201: No provider for Injector found in NodeInjector/ : + /No provider for Injector/; + expect(() => TestBed.createComponent(ParentComponent)) + .not.toThrowError(expectedErrorMessage); + }); + }); + + describe('ElementRef', () => { + // While tokens like `ElementRef` make sense only in a context of a NodeInjector, + // ViewEngine also used `ModuleInjector` tree to lookup such tokens. In Ivy we replicate + // this behavior for now to avoid breaking changes. + it('should lookup module injector in case @SkipSelf is used for `ElementRef` token and Component has no parent', + () => { + let componentElement: ElementRef; + let moduleElement: ElementRef; + @Component({template: '
component
'}) + class MyComponent { + constructor(@SkipSelf() public el: ElementRef) { + componentElement = el; + } + } + + @NgModule({ + declarations: [MyComponent], + providers: [{ + provide: ElementRef, + useValue: {from: 'NG_MODULE'}, + }] + }) + class MyModule { + constructor(public el: ElementRef) { + moduleElement = el; + } + } + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + + expect((moduleElement! as any).from).toBe('NG_MODULE'); + expect((componentElement! as any).from).toBe('NG_MODULE'); + }); + + it('should return host node when @SkipSelf is used for `ElementRef` token and Component has no parent node', + () => { + let parentElement: ElementRef; + let componentElement: ElementRef; + @Component({selector: 'child', template: '...'}) + class MyComponent { + constructor(@SkipSelf() public el: ElementRef) { + componentElement = el; + } + } + + @Component({ + template: '', + }) + class ParentComponent { + constructor(public el: ElementRef) { + parentElement = el; + } + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [ParentComponent, MyComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + expect(componentElement!).toEqual(parentElement!); + }); + + it('should @SkipSelf on child directive node when injecting ElementRef on nested parent directive', + () => { + let parentRef: ElementRef; + let childRef: ElementRef; + + @Directive({selector: '[parent]'}) + class ParentDirective { + constructor(elementRef: ElementRef) { + parentRef = elementRef; + } + } + + @Directive({selector: '[child]'}) + class ChildDirective { + constructor(@SkipSelf() elementRef: ElementRef) { + childRef = elementRef; + } + } + + @Component({template: '
parent child
'}) + class MyComp { + } + + TestBed.configureTestingModule( + {declarations: [ParentDirective, ChildDirective, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + // Assert against the `nativeElement` since Ivy always returns a new ElementRef. + expect(childRef!.nativeElement).toBe(parentRef!.nativeElement); + expect(childRef!.nativeElement.tagName).toBe('DIV'); + }); + }); + + describe('@SkipSelf when parent contains embedded views', () => { + it('should work for `ElementRef` token', () => { + let requestedElementRef: ElementRef; + @Component({ + selector: 'child', + template: '...', + }) + class ChildComponent { + constructor(@SkipSelf() public elementRef: ElementRef) { + requestedElementRef = elementRef; + } + } + @Component({ + selector: 'root', + template: '
', + }) + class ParentComponent { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + expect(requestedElementRef!.nativeElement).toEqual(fixture.nativeElement.firstChild); + expect(requestedElementRef!.nativeElement.tagName).toEqual('DIV'); + }); + + it('should work for `ElementRef` token with expanded *ngIf', () => { + let requestedElementRef: ElementRef; + @Component({ + selector: 'child', + template: '...', + }) + class ChildComponent { + constructor(@SkipSelf() public elementRef: ElementRef) { + requestedElementRef = elementRef; + } + } + @Component({ + selector: 'root', + template: '
', + }) + class ParentComponent { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + expect(requestedElementRef!.nativeElement).toEqual(fixture.nativeElement.firstChild); + expect(requestedElementRef!.nativeElement.tagName).toEqual('DIV'); + }); + + it('should work for `ViewContainerRef` token', () => { + let requestedRef: ViewContainerRef; + @Component({ + selector: 'child', + template: '...', + }) + class ChildComponent { + constructor(@SkipSelf() public ref: ViewContainerRef) { + requestedRef = ref; + } + } + + @Component({ + selector: 'root', + template: '
', + }) + class ParentComponent { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + if (ivyEnabled) { + expect(requestedRef!.element.nativeElement).toBe(fixture.nativeElement.firstChild); + expect(requestedRef!.element.nativeElement.tagName).toBe('DIV'); + } else { + expect(requestedRef!).toBeNull(); + } + }); + + it('should work for `ChangeDetectorRef` token', () => { + let requestedChangeDetectorRef: ChangeDetectorRef; + @Component({ + selector: 'child', + template: '...', + }) + class ChildComponent { + constructor(@SkipSelf() public changeDetectorRef: ChangeDetectorRef) { + requestedChangeDetectorRef = changeDetectorRef; + } + } + + @Component({ + selector: 'root', + template: '', + }) + class ParentComponent { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + const {context} = requestedChangeDetectorRef! as ViewRefInternal; + expect(context).toBe(fixture.componentInstance); + }); + + // this works consistently between VE and Ivy + it('should work for Injectors', () => { + let childComponentInjector: Injector; + let parentComponentInjector: Injector; + @Component({ + selector: 'parent', + template: '', + providers: [{ + provide: 'token', + useValue: 'PARENT', + }] + }) + class ParentComponent { + constructor(public injector: Injector) { + parentComponentInjector = injector; + } + } + + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class ChildComponent { + constructor(@SkipSelf() public injector: Injector) { + childComponentInjector = injector; + } + } + + TestBed.configureTestingModule({ + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + expect(childComponentInjector!.get('token')) + .toBe(parentComponentInjector!.get('token')); + }); + + it('should work for Injectors with expanded *ngIf', () => { + let childComponentInjector: Injector; + let parentComponentInjector: Injector; + @Component({ + selector: 'parent', + template: '', + providers: [{ + provide: 'token', + useValue: 'PARENT', + }] + }) + class ParentComponent { + constructor(public injector: Injector) { + parentComponentInjector = injector; + } + } + + @Component({ + selector: 'child', + template: '...', + providers: [{ + provide: 'token', + useValue: 'CHILD', + }] + }) + class ChildComponent { + constructor(@SkipSelf() public injector: Injector) { + childComponentInjector = injector; + } + } + + TestBed.configureTestingModule({ + declarations: [ParentComponent, ChildComponent], + }); + const fixture = TestBed.createComponent(ParentComponent); + fixture.detectChanges(); + + expect(childComponentInjector!.get('token')) + .toBe(parentComponentInjector!.get('token')); + }); + }); + + describe('TemplateRef', () => { + // SkipSelf doesn't make sense to use with TemplateRef since you + // can't inject TemplateRef on a regular element and you can initialize + // a child component on a nested `` only when a component/directive + // on a parent `` is initialized. + it('should throw when using @SkipSelf for TemplateRef', () => { + @Directive({selector: '[dir]', exportAs: 'dir'}) + class MyDir { + constructor(@SkipSelf() public templateRef: TemplateRef) {} + } + + @Component({selector: '[child]', template: ''}) + class ChildComp { + constructor(public templateRef: TemplateRef) {} + @ViewChild(MyDir) directive!: MyDir; + } + + @Component({ + selector: 'root', + template: '
', + }) + class MyComp { + @ViewChild(ChildComp) child!: ChildComp; + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [MyDir, ChildComp, MyComp], + }); + // Ivy has different error message when dependency is not found + const expectedErrorMessage = ivyEnabled ? /NG0201: No provider for TemplateRef found/ : + /No provider for TemplateRef/; + expect(() => { + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + }).toThrowError(expectedErrorMessage); + }); + + it('should throw when SkipSelf and no parent TemplateRef', () => { + @Directive({selector: '[dirA]', exportAs: 'dirA'}) + class DirA { + constructor(@SkipSelf() public templateRef: TemplateRef) {} + } + + @Component({ + selector: 'root', + template: '', + }) + class MyComp { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [DirA, MyComp], + }); + // Ivy has different error message when dependency is not found + const expectedErrorMessage = ivyEnabled ? /NG0201: No provider for TemplateRef found/ : + /No provider for TemplateRef/; + expect(() => { + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + }).toThrowError(expectedErrorMessage); + }); + + it('should not throw when SkipSelf and Optional', () => { + let directiveTemplateRef; + @Directive({selector: '[dirA]', exportAs: 'dirA'}) + class DirA { + constructor(@SkipSelf() @Optional() templateRef: TemplateRef) { + directiveTemplateRef = templateRef; + } + } + + @Component({ + selector: 'root', + template: '', + }) + class MyComp { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [DirA, MyComp], + }); + + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(directiveTemplateRef).toBeNull(); + }); + + it('should not throw when SkipSelf, Optional, and Host', () => { + @Directive({selector: '[dirA]', exportAs: 'dirA'}) + class DirA { + constructor(@SkipSelf() @Optional() @Host() public templateRef: TemplateRef) {} + } + + @Component({ + selector: 'root', + template: '', + }) + class MyComp { + } + + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [DirA, MyComp], + }); + + expect(() => TestBed.createComponent(MyComp)).not.toThrowError(); + }); + }); + + describe('ViewContainerRef', () => { + it('should support @SkipSelf when injecting ViewContainerRef', () => { + let parentViewContainer: ViewContainerRef; + let childViewContainer: ViewContainerRef; + + @Directive({selector: '[parent]'}) + class ParentDirective { + constructor(vc: ViewContainerRef) { + parentViewContainer = vc; + } + } + + @Directive({selector: '[child]'}) + class ChildDirective { + constructor(@SkipSelf() vc: ViewContainerRef) { + childViewContainer = vc; + } + } + + @Component({template: '
parent child
'}) + class MyComp { + } + + TestBed.configureTestingModule( + {declarations: [ParentDirective, ChildDirective, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + // Assert against the `element` since Ivy always returns a new ViewContainerRef. + expect(childViewContainer!.element.nativeElement) + .toBe(parentViewContainer!.element.nativeElement); + expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV'); + }); + + it('should get ViewContainerRef using @SkipSelf and @Host', () => { + let parentViewContainer: ViewContainerRef; + let childViewContainer: ViewContainerRef; + + @Directive({selector: '[parent]'}) + class ParentDirective { + constructor(vc: ViewContainerRef) { + parentViewContainer = vc; + } + } + + @Directive({selector: '[child]'}) + class ChildDirective { + constructor(@SkipSelf() @Host() vc: ViewContainerRef) { + childViewContainer = vc; + } + } + + @Component({template: '
parent child
'}) + class MyComp { + } + + TestBed.configureTestingModule( + {declarations: [ParentDirective, ChildDirective, MyComp]}); + + if (ivyEnabled) { + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + // Assert against the `element` since Ivy always returns a new ViewContainerRef. + expect(childViewContainer!.element.nativeElement) + .toBe(parentViewContainer!.element.nativeElement); + expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV'); + } else { + // Template parse errors happen in VE + // "
parent [ERROR ->]child
" + expect(() => TestBed.createComponent(MyComp)) + .toThrowError(/No provider for ViewContainerRef/); + } + }); + + it('should get ViewContainerRef using @SkipSelf and @Host on parent', () => { + let parentViewContainer: ViewContainerRef; + + @Directive({selector: '[parent]'}) + class ParentDirective { + constructor(@SkipSelf() vc: ViewContainerRef) { + parentViewContainer = vc; + } + } + + @Component({template: '
parent
'}) + class MyComp { + } + + TestBed.configureTestingModule({declarations: [ParentDirective, MyComp]}); + + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + if (ivyEnabled) { + // Assert against the `element` since Ivy always returns a new ViewContainerRef. + expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV'); + } else { + // VE Doesn't throw, but the ref is null + expect(parentViewContainer!).toBeNull(); + } + }); + + it('should throw when injecting ViewContainerRef using @SkipSelf and no ViewContainerRef are available in a current view', + () => { + @Component({template: 'component'}) + class MyComp { + constructor(@SkipSelf() vc: ViewContainerRef) {} + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + + expect(() => TestBed.createComponent(MyComp)) + .toThrowError(/No provider for ViewContainerRef/); + }); + }); + + describe('ChangeDetectorRef', () => { + it('should support @SkipSelf when injecting ChangeDetectorRef', () => { + let parentRef: ChangeDetectorRef|undefined; + let childRef: ChangeDetectorRef|undefined; + + @Directive({selector: '[parent]'}) + class ParentDirective { + constructor(cdr: ChangeDetectorRef) { + parentRef = cdr; + } + } + + @Directive({selector: '[child]'}) + class ChildDirective { + constructor(@SkipSelf() cdr: ChangeDetectorRef) { + childRef = cdr; + } + } + + @Component({template: '
parent child
'}) + class MyComp { + } + + TestBed.configureTestingModule( + {declarations: [ParentDirective, ChildDirective, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + // Assert against the `rootNodes` since Ivy always returns a new ChangeDetectorRef. + expect((parentRef as ViewRefInternal).rootNodes) + .toEqual((childRef as ViewRefInternal).rootNodes); + }); + + it('should inject host component ChangeDetectorRef when @SkipSelf', () => { + let childRef: ChangeDetectorRef|undefined; + + @Component({selector: 'child', template: '...'}) + class ChildComp { + constructor(@SkipSelf() cdr: ChangeDetectorRef) { + childRef = cdr; + } + } + + @Component({template: '
'}) + class MyComp { + constructor(public cdr: ChangeDetectorRef) {} + } + + TestBed.configureTestingModule({declarations: [ChildComp, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + // Assert against the `rootNodes` since Ivy always returns a new ChangeDetectorRef. + expect((childRef as ViewRefInternal).rootNodes) + .toEqual((fixture.componentInstance.cdr as ViewRefInternal).rootNodes); + }); + + it('should throw when ChangeDetectorRef and @SkipSelf and not found', () => { + @Component({template: '
'}) + class MyComponent { + constructor(@SkipSelf() public injector: ChangeDetectorRef) {} + } + + @NgModule({ + declarations: [MyComponent], + }) + class MyModule { + } + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + + expect(() => TestBed.createComponent(MyComponent)) + .toThrowError(/No provider for ChangeDetectorRef/); + }); + + it('should lookup module injector in case @SkipSelf is used for `ChangeDetectorRef` token and Component has no parent', + () => { + let componentCDR: ChangeDetectorRef; + let moduleCDR: ChangeDetectorRef; + @Component({selector: 'child', template: '...'}) + class MyComponent { + constructor(@SkipSelf() public injector: ChangeDetectorRef) { + componentCDR = injector; + } + } + + @NgModule({ + declarations: [MyComponent], + providers: [{ + provide: ChangeDetectorRef, + useValue: {from: 'NG_MODULE'}, + }] + }) + class MyModule { + constructor(public injector: ChangeDetectorRef) { + moduleCDR = injector; + } + } + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + + expect((moduleCDR! as any).from).toBe('NG_MODULE'); + expect((componentCDR! as any).from).toBe('NG_MODULE'); + }); + }); + + describe('viewProviders', () => { + it('should support @SkipSelf when using viewProviders', () => { + @Component({ + selector: 'child', + template: '{{ blah | json }}
{{ foo | json }}
{{ bar | json }}', + providers: [{provide: 'Blah', useValue: 'Blah as Provider'}], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Child { + constructor( + @Inject('Blah') public blah: String, + @Inject('Foo') public foo: String, + @SkipSelf() @Inject('Bar') public bar: String, + ) {} + } + + @Component({ + selector: 'parent', + template: '', + providers: [ + {provide: 'Blah', useValue: 'Blah as provider'}, + {provide: 'Bar', useValue: 'Bar as Provider'}, + ], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Parent { + } + + @Component({selector: 'my-app', template: ''}) + class MyApp { + @ViewChild(Parent) parent!: Parent; + @ViewChild(Child) child!: Child; + } + + TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const child = fixture.componentInstance.child; + if (ivyEnabled) { + expect(child.bar).toBe('Bar as Provider'); + } else { + // this seems like a ViewEngine bug + expect(child.bar).toBe('Bar as ViewProvider'); + } + }); + + it('should throw when @SkipSelf and no accessible viewProvider', () => { + @Component({ + selector: 'child', + template: '{{ blah | json }}
{{ foo | json }}
{{ bar | json }}', + providers: [{provide: 'Blah', useValue: 'Blah as Provider'}], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Child { + constructor( + @Inject('Blah') public blah: String, + @Inject('Foo') public foo: String, + @SkipSelf() @Inject('Bar') public bar: String, + ) {} + } + + @Component({ + selector: 'parent', + template: '', + providers: [{provide: 'Blah', useValue: 'Blah as provider'}], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Parent { + } + + @Component({selector: 'my-app', template: ''}) + class MyApp { + } + + TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]}); + + expect(() => TestBed.createComponent(MyApp)).toThrowError(/No provider for Bar/); + }); + + it('should not throw when @SkipSelf and @Optional with no accessible viewProvider', + () => { + @Component({ + selector: 'child', + template: '{{ blah | json }}
{{ foo | json }}
{{ bar | json }}', + providers: [{provide: 'Blah', useValue: 'Blah as Provider'}], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Child { + constructor( + @Inject('Blah') public blah: String, + @Inject('Foo') public foo: String, + @SkipSelf() @Optional() @Inject('Bar') public bar: String, + ) {} + } + + @Component({ + selector: 'parent', + template: '', + providers: [{provide: 'Blah', useValue: 'Blah as provider'}], + viewProviders: [ + {provide: 'Foo', useValue: 'Foo as ViewProvider'}, + {provide: 'Bar', useValue: 'Bar as ViewProvider'}, + ] + }) + class Parent { + } + + @Component({selector: 'my-app', template: ''}) + class MyApp { + } + + TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]}); + + expect(() => TestBed.createComponent(MyApp)).not.toThrowError(/No provider for Bar/); + }); + }); + }); + describe('@Host', () => { @Directive({selector: '[dirA]'}) class DirectiveA { @@ -1134,7 +2099,8 @@ describe('di', () => { expect(provider!.getMessage()).toBe('bar'); - // ViewEngine incorrectly uses the original class DI config, instead of the one from useClass. + // ViewEngine incorrectly uses the original class DI config, instead of the one from + // useClass. if (ivyEnabled) { expect(provider!.dep.name).toBe('BarServiceDep'); } @@ -1180,7 +2146,8 @@ describe('di', () => { expect(directProvider!.getMessage()).toBe('bar'); expect(overriddenProvider!.getMessage()).toBe('foo'); - // ViewEngine incorrectly uses the original class DI config, instead of the one from useClass. + // ViewEngine incorrectly uses the original class DI config, instead of the one from + // useClass. if (ivyEnabled) { expect(directProvider!.dep.name).toBe('BarServiceDep'); expect(overriddenProvider!.dep.name).toBe('FooServiceDep'); @@ -1811,8 +2778,9 @@ describe('di', () => { providers: [{ provide: LOCALE_ID, useFactory: () => 'ja-JP', - // Note: `LOCALE_ID` is also provided within APPLICATION_MODULE_PROVIDERS, so we use it here - // as a dep and making sure it doesn't cause cyclic dependency (since @SkipSelf is present) + // Note: `LOCALE_ID` is also provided within APPLICATION_MODULE_PROVIDERS, so we use it + // here as a dep and making sure it doesn't cause cyclic dependency (since @SkipSelf is + // present) deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] }] }) diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index cc440fb4aa..d4f067f254 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -14,6 +14,9 @@ { "name": "EMPTY_OBJ" }, + { + "name": "InjectFlags" + }, { "name": "Module" }, @@ -173,6 +176,9 @@ { "name": "getCurrentTNodePlaceholderOk" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, diff --git a/packages/core/test/bundling/forms/bundle.golden_symbols.json b/packages/core/test/bundling/forms/bundle.golden_symbols.json index 0e06d058e6..4e692ada58 100644 --- a/packages/core/test/bundling/forms/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms/bundle.golden_symbols.json @@ -983,6 +983,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFactoryDef" }, @@ -1286,6 +1289,9 @@ { "name": "localeEn" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -1379,6 +1385,9 @@ { "name": "normalizeValidators" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "observable" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index dd2c07f81c..6f5521b982 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -11,6 +11,9 @@ { "name": "EMPTY_OBJ" }, + { + "name": "InjectFlags" + }, { "name": "NG_COMP_DEF" }, @@ -128,6 +131,9 @@ { "name": "getCurrentTNodePlaceholderOk" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 40f40ecf01..5daed97b13 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1292,6 +1292,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFactoryDef" }, @@ -1607,6 +1610,9 @@ { "name": "locateDirectiveOrProvider" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -1703,6 +1709,9 @@ { "name": "normalizeQueryParams" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "observable" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 197a374cf8..c053fb45b0 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -353,6 +353,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, @@ -542,6 +545,9 @@ { "name": "leaveViewLight" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -587,6 +593,9 @@ { "name": "noSideEffects" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "readPatchedData" },