feat(ivy): expose a series of helpful application inspection tools (#25919)

PR Close #25919
This commit is contained in:
Matias Niemelä 2018-09-24 14:51:54 -07:00 committed by Kara Erickson
parent cf095d982d
commit 0c344715e5
7 changed files with 894 additions and 135 deletions

View File

@ -3,13 +3,13 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "ng build --prod --progress false", "build": "ng build --prod --progress=false",
"e2e": "ng e2e", "e2e": "ng e2e",
"lint": "ng lint", "lint": "ng lint",
"ng": "ng", "ng": "ng",
"postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG", "postinstall": "webdriver-manager update --gecko=false --standalone=false $CHROMEDRIVER_VERSION_ARG",
"start": "ng serve", "start": "ng serve",
"test": "ng test && ng e2e --webdriver-update=false && ng e2e --prod --webdriver-update=false" "test": "ng test --progress=false && ng e2e --webdriver-update=false && ng e2e --prod --webdriver-update=false"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -28,7 +28,7 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.5.0", "@angular-devkit/build-angular": "~0.5.0",
"@angular/cli": "^6.0.0-rc.5", "@angular/cli": "7.0.0-beta.4",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/language-service": "file:../../dist/packages-dist/language-service", "@angular/language-service": "file:../../dist/packages-dist/language-service",
"@types/jasmine": "~2.8.3", "@types/jasmine": "~2.8.3",
@ -47,4 +47,4 @@
"tslint": "~5.9.1", "tslint": "~5.9.1",
"typescript": "file:../../node_modules/typescript" "typescript": "file:../../node_modules/typescript"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -352,7 +352,8 @@ function getLNodeFromViewData(lViewData: LViewData, lElementIndex: number): LEle
* Returns a collection of directive index values that are used on the element * Returns a collection of directive index values that are used on the element
* (which is referenced by the lNodeIndex) * (which is referenced by the lNodeIndex)
*/ */
function discoverDirectiveIndices(lViewData: LViewData, lNodeIndex: number): number[]|null { export function discoverDirectiveIndices(
lViewData: LViewData, lNodeIndex: number, includeComponents?: boolean): number[]|null {
const directivesAcrossView = lViewData[DIRECTIVES]; const directivesAcrossView = lViewData[DIRECTIVES];
const tNode = lViewData[TVIEW].data[lNodeIndex] as TNode; const tNode = lViewData[TVIEW].data[lNodeIndex] as TNode;
if (directivesAcrossView && directivesAcrossView.length) { if (directivesAcrossView && directivesAcrossView.length) {
@ -373,12 +374,20 @@ function discoverDirectiveIndices(lViewData: LViewData, lNodeIndex: number): num
return null; return null;
} }
function discoverDirectives(lViewData: LViewData, directiveIndices: number[]): number[]|null { /**
* Returns a list of directives extracted from the given view based on the
* provided list of directive index values.
*
* @param lViewData The target view data
* @param indices A collection of directive index values which will be used to
* figure out the directive instances
*/
export function discoverDirectives(lViewData: LViewData, indices: number[]): number[]|null {
const directives: any[] = []; const directives: any[] = [];
const directiveInstances = lViewData[DIRECTIVES]; const directiveInstances = lViewData[DIRECTIVES];
if (directiveInstances) { if (directiveInstances) {
for (let i = 0; i < directiveIndices.length; i++) { for (let i = 0; i < indices.length; i++) {
const directiveIndex = directiveIndices[i]; const directiveIndex = indices[i];
const directive = directiveInstances[directiveIndex]; const directive = directiveInstances[directiveIndex];
directives.push(directive); directives.push(directive);
} }

View File

@ -0,0 +1,143 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '../di/injector';
import {assertDefined} from './assert';
import {LContext, discoverDirectiveIndices, discoverDirectives, getContext, isComponentInstance, readPatchedLViewData} from './context_discovery';
import {LElementNode, TNode, TNodeFlags} from './interfaces/node';
import {CONTEXT, FLAGS, INJECTOR, LViewData, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view';
/**
* NOTE: The following functions might not be ideal for core usage in Angular...
*
* Each function below is designed
*/
/**
* Returns the component instance associated with the target.
*
* If a DOM is used then it will return the component that
* owns the view where the element is situated.
* If a component instance is used then it will return the
* instance of the parent component depending on where
* the component instance is exists in a template.
* If a directive instance is used then it will return the
* component that contains that directive in it's template.
*/
export function getComponent<T = {}>(target: {}): T|null {
const context = loadContext(target) !;
if (context.component === undefined) {
let lViewData = context.lViewData;
while (lViewData) {
const ctx = lViewData ![CONTEXT] !as{};
if (ctx && isComponentInstance(ctx)) {
context.component = ctx;
break;
}
lViewData = lViewData ![PARENT] !;
}
if (context.component === undefined) {
context.component = null;
}
}
return context.component as T;
}
/**
* Returns the host component instance associated with the target.
*
* This will only return a component instance of the DOM node
* contains an instance of a component on it.
*/
export function getHostComponent<T = {}>(target: {}): T|null {
const context = loadContext(target);
const tNode = context.lViewData[TVIEW].data[context.lNodeIndex] as TNode;
if (tNode.flags & TNodeFlags.isComponent) {
const lNode = context.lViewData[context.lNodeIndex] as LElementNode;
return lNode.data ![CONTEXT] as any as T;
}
return null;
}
/**
* Returns the `RootContext` instance that is associated with
* the application where the target is situated.
*/
export function getRootContext(target: {}): RootContext {
const context = loadContext(target) !;
const rootLViewData = getRootView(context.lViewData);
return rootLViewData[CONTEXT] as RootContext;
}
/**
* Returns a list of all the components in the application
* that are have been bootstrapped.
*/
export function getRootComponents(target: {}): any[] {
return [...getRootContext(target).components];
}
/**
* Returns the injector instance that is associated with
* the element, component or directive.
*/
export function getInjector(target: {}): Injector|null {
const context = loadContext(target) !;
return context.lViewData[INJECTOR] || null;
}
/**
* Returns a list of all the directives that are associated
* with the underlying target element.
*/
export function getDirectives(target: {}): Array<{}> {
const context = loadContext(target) !;
if (context.directives === undefined) {
context.directiveIndices = discoverDirectiveIndices(context.lViewData, context.lNodeIndex);
context.directives = context.directiveIndices ?
discoverDirectives(context.lViewData, context.directiveIndices) :
null;
}
return context.directives || [];
}
function loadContext(target: {}): LContext {
const context = getContext(target);
if (!context) {
throw new Error(
ngDevMode ? 'Unable to find the given context data for the given target' :
'Invalid ng target');
}
return context;
}
/**
* Retrieve the root view from any component by walking the parent `LViewData` until
* reaching the root `LViewData`.
*
* @param componentOrView any component or view
*/
export function getRootView(componentOrView: LViewData | {}): LViewData {
let lViewData: LViewData;
if (Array.isArray(componentOrView)) {
ngDevMode && assertDefined(componentOrView, 'lViewData');
lViewData = componentOrView as LViewData;
} else {
ngDevMode && assertDefined(componentOrView, 'component');
lViewData = readPatchedLViewData(componentOrView) !;
}
while (lViewData && !(lViewData[FLAGS] & LViewFlags.IsRoot)) {
lViewData = lViewData[PARENT] !;
}
return lViewData;
}

View File

@ -11,9 +11,9 @@ import './ng_dev_mode';
import {QueryList} from '../linker'; import {QueryList} from '../linker';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert'; import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert';
import {attachPatchData, getLElementFromComponent, readElementValue, readPatchedLViewData} from './context_discovery'; import {attachPatchData, getLElementFromComponent, readElementValue, readPatchedLViewData} from './context_discovery';
import {getRootView} from './discovery_utils';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
@ -28,9 +28,7 @@ import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, findComponentView, getContainerNode, getHostElementNode, getLViewChild, getParentOrContainerNode, getRenderParent, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, findComponentView, getContainerNode, getHostElementNode, getLViewChild, getParentOrContainerNode, getRenderParent, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling';
import {assertDataInRangeInternal, getLNode, getRootContext, getRootView, isContentQueryHost, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {assertDataInRangeInternal, getLNode, isContentQueryHost, isDifferent, loadElementInternal, loadInternal, stringify} from './util';
import {ViewRef} from './view_ref';
/** /**

View File

@ -654,10 +654,10 @@
"name": "getRendererFactory" "name": "getRendererFactory"
}, },
{ {
"name": "getRootContext" "name": "getRootContext$1"
}, },
{ {
"name": "getRootView" "name": "getRootView$1"
}, },
{ {
"name": "getStyleSanitizer" "name": "getStyleSanitizer"

View File

@ -0,0 +1,269 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticInjector} from '../../src/di/injector';
import {getComponent, getDirectives, getHostComponent, getInjector, getRootComponents} from '../../src/render3/discovery_utils';
import {RenderFlags, defineComponent, defineDirective} from '../../src/render3/index';
import {element} from '../../src/render3/instructions';
import {ComponentFixture} from './render_util';
describe('discovery utils', () => {
describe('getComponent()', () => {
it('should return the component instance for a DOM element', () => {
class InnerComp {
static ngComponentDef = defineComponent({
type: InnerComp,
selectors: [['inner-comp']],
factory: () => new InnerComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: InnerComp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'inner-comp');
}
},
directives: [InnerComp]
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const hostElm = fixture.hostElement;
const innerCompElm = hostElm.querySelector('inner-comp');
const component = fixture.component;
expect(getComponent(innerCompElm !) !).toBe(component);
expect(getComponent(hostElm) !).toBeFalsy();
});
});
describe('getRootComponents()', () => {
it('should return a list of the root components of the application from an element', () => {
let innerComp: InnerComp;
class InnerComp {
static ngComponentDef = defineComponent({
type: InnerComp,
selectors: [['inner-comp']],
factory: () => innerComp = new InnerComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: InnerComp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'inner-comp');
}
},
directives: [InnerComp]
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const hostElm = fixture.hostElement;
const innerElm = hostElm.querySelector('inner-comp') !;
const divElm = hostElm.querySelector('div') !;
const component = fixture.component;
expect(getRootComponents(hostElm) !).toEqual([component]);
expect(getRootComponents(innerElm) !).toEqual([component]);
expect(getRootComponents(divElm) !).toEqual([component]);
});
});
describe('getDirectives()', () => {
it('should return a list of the directives that are on the given element', () => {
let myDir1Instance: MyDir1|null = null;
let myDir2Instance: MyDir2|null = null;
let myDir3Instance: MyDir2|null = null;
class MyDir1 {
static ngDirectiveDef = defineDirective({
type: MyDir1,
selectors: [['', 'my-dir-1', '']],
factory: () => myDir1Instance = new MyDir1()
});
}
class MyDir2 {
static ngDirectiveDef = defineDirective({
type: MyDir2,
selectors: [['', 'my-dir-2', '']],
factory: () => myDir2Instance = new MyDir2()
});
}
class MyDir3 {
static ngDirectiveDef = defineDirective({
type: MyDir3,
selectors: [['', 'my-dir-3', '']],
factory: () => myDir3Instance = new MyDir2()
});
}
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['my-dir-1', '', 'my-dir-2', '']);
element(1, 'div', ['my-dir-3']);
}
},
directives: [MyDir1, MyDir2, MyDir3]
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const hostElm = fixture.hostElement;
const elements = hostElm.querySelectorAll('div');
const elm1 = elements[0];
const elm1Dirs = getDirectives(elm1);
expect(elm1Dirs).toContain(myDir1Instance !);
expect(elm1Dirs).toContain(myDir2Instance !);
const elm2 = elements[1];
const elm2Dirs = getDirectives(elm2);
expect(elm2Dirs).toContain(myDir3Instance !);
});
});
describe('getHostComponent()', () => {
it('should return the component instance for a DOM element', () => {
let innerComp: InnerComp;
class InnerComp {
static ngComponentDef = defineComponent({
type: InnerComp,
selectors: [['inner-comp']],
factory: () => innerComp = new InnerComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: InnerComp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'inner-comp');
}
},
directives: [InnerComp]
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
const hostElm = fixture.hostElement;
const innerElm = hostElm.querySelector('inner-comp') !;
const divElm = hostElm.querySelector('div') !;
const component = fixture.component;
expect(getHostComponent(hostElm) !).toBe(component);
expect(getHostComponent(innerElm) !).toBe(innerComp !);
expect(getHostComponent(divElm) !).toBeFalsy();
});
});
describe('getInjector()', () => {
it('should return the instance of the injector that was passed into the component', () => {
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
const injector = new StaticInjector([]);
const fixture = new ComponentFixture(Comp, {injector});
fixture.update();
expect(getInjector(fixture.hostElement) !).toBe(injector);
});
it('should return null when there is no injector passed into a component', () => {
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
}
});
}
const fixture = new ComponentFixture(Comp);
fixture.update();
expect(getInjector(fixture.hostElement)).toEqual(null);
});
});
});