refactor(devtools): migrate devtools to prettier formatting (#53945)

Migrate formatting to prettier for devtools from clang-format

PR Close #53945
This commit is contained in:
Joey Perrott 2024-01-16 21:35:47 +00:00 committed by Pawel Kozlowski
parent 4be253483d
commit 711cb41626
161 changed files with 10689 additions and 9615 deletions

View file

@ -7,6 +7,7 @@ export const format: FormatConfig = {
'prettier': {
'matchers': [
'**/*.{yaml,yml}',
'devtools/**/*.{js,ts}',
'tools/**/*.{js,ts}',
'modules/**/*.{js,ts}',
'scripts/**/*.{js,ts}',
@ -38,6 +39,7 @@ export const format: FormatConfig = {
'!adev/**',
// Migrated to prettier
'!devtools/**/*.{js,ts}',
'!tools/**/*.{js,ts}',
'!modules/**/*.{js,ts}',
'!scripts/**/*.{js,ts}',

View file

@ -24,8 +24,8 @@ describe('Comment nodes', () => {
it('should find comment nodes when the setting is enabled', () => {
showComments();
cy.get('.tree-wrapper')
.find('.tree-node:contains("#comment")')
.its('length')
.should('not.eq', 0);
.find('.tree-node:contains("#comment")')
.its('length')
.should('not.eq', 0);
});
});

View file

@ -19,27 +19,29 @@ describe('Tracking items from application to component tree', () => {
});
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.its('length')
.should('eq', 2);
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.its('length')
.should('eq', 2);
});
it('should be able to detect a new todo from user and add it to the tree', () => {
cy.enter('#sample-app')
.then((getBody) => {
getBody().find('input.new-todo').type('Buy cookies{enter}');
})
.then(() => {
cy.enter('#sample-app').then((getBody) => {
getBody().find('app-todo').contains('Buy milk');
.then((getBody) => {
getBody().find('input.new-todo').type('Buy cookies{enter}');
})
.then(() => {
cy.enter('#sample-app').then((getBody) => {
getBody().find('app-todo').contains('Buy milk');
getBody().find('app-todo').contains('Build something fun!');
getBody().find('app-todo').contains('Build something fun!');
getBody().find('app-todo').contains('Buy cookies');
});
getBody().find('app-todo').contains('Buy cookies');
});
});
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")')
.should('have.length', 3);
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")').should(
'have.length',
3,
);
});
});

View file

@ -83,9 +83,9 @@ describe('Search items in component tree', () => {
const amountOfBreadcrumbButtons = 4;
const amountOfScrollButtons = 2;
cy.get('ng-breadcrumbs')
.find('button')
.its('length')
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
.find('button')
.its('length')
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
// should display correct text in explorer panel
checkComponentName('app-todos');

View file

@ -18,9 +18,9 @@ describe('node selection', () => {
cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist');
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1);
@ -39,9 +39,9 @@ describe('node selection', () => {
});
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.last()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.last()
.click({force: true});
cy.enter('#sample-app').then((getBody) => {
getBody().find('app-todo:contains("Buy milk")').find('.destroy').click();
@ -52,86 +52,100 @@ describe('node selection', () => {
it('should select nodes with same name', () => {
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.last()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.last()
.click({force: true});
cy.get('ng-property-view').last().find('mat-tree-node:contains("todo")').click();
cy.get('ng-property-view')
.last()
.find('mat-tree-node:contains("Build something fun!")')
.its('length')
.should('eq', 1);
.last()
.find('mat-tree-node:contains("Build something fun!")')
.its('length')
.should('eq', 1);
});
});
describe('breadcrumb logic', () => {
it('should overflow when breadcrumb list is long enough', () => {
cy.get('.tree-wrapper')
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs').find('.breadcrumbs').then((breadcrumbsContainer) => {
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs')
.find('.breadcrumbs')
.then((breadcrumbsContainer) => {
const hasOverflowX = () =>
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
expect(hasOverflowX()).to.be.true;
});
});
});
});
it('should scroll right when right scroll button is clicked', () => {
cy.get('.tree-wrapper')
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs')
.find('.breadcrumbs')
.then((el) => {
el[0].style.scrollBehavior = 'auto';
})
.then((breadcrumbsContainer) => {
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
expect(scrollLeft()).to.eql(0);
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs')
.find('.breadcrumbs')
.then((el) => {
el[0].style.scrollBehavior = 'auto';
})
.then((breadcrumbsContainer) => {
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
expect(scrollLeft()).to.eql(0);
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
expect(scrollLeft()).to.be.greaterThan(0);
});
cy.get('ng-breadcrumbs')
.find('.scroll-button')
.last()
.click()
.then(() => {
expect(scrollLeft()).to.be.greaterThan(0);
});
});
});
});
});
it('should scroll left when left scroll button is clicked', () => {
cy.get('.tree-wrapper')
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs')
.find('.breadcrumbs')
.then((el) => {
el[0].style.scrollBehavior = 'auto';
})
.then((breadcrumbsContainer) => {
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
expect(scrollLeft()).to.eql(0);
.find('.tree-node:contains("div[TooltipDirective]")')
.last()
.click({force: true})
.then(() => {
cy.get('ng-breadcrumbs')
.find('.breadcrumbs')
.then((el) => {
el[0].style.scrollBehavior = 'auto';
})
.then((breadcrumbsContainer) => {
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
expect(scrollLeft()).to.eql(0);
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
expect(scrollLeft()).to.be.greaterThan(0);
cy.get('ng-breadcrumbs')
.find('.scroll-button')
.last()
.click()
.then(() => {
expect(scrollLeft()).to.be.greaterThan(0);
cy.get('ng-breadcrumbs').find('.scroll-button').first().click().then(() => {
cy.get('ng-breadcrumbs')
.find('.scroll-button')
.first()
.click()
.then(() => {
expect(scrollLeft()).to.eql(0);
});
});
});
});
});
});
});
});
});

View file

@ -17,9 +17,9 @@ describe('edit properties of directive in the property view tab', () => {
beforeEach(() => {
// select todo node in component tree
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
});
it('should be able to enable editMode', () => {
@ -28,13 +28,13 @@ describe('edit properties of directive in the property view tab', () => {
});
cy.get('.explorer-panel:contains("app-todo")')
.find('ng-property-view mat-tree-node:contains("editMode")')
.find('ng-property-editor .editor')
.click({force: true})
.find('.editor-input')
.clear()
.type('true')
.type('{enter}');
.find('ng-property-view mat-tree-node:contains("editMode")')
.find('ng-property-editor .editor')
.click({force: true})
.find('.editor-input')
.clear()
.type('true')
.type('{enter}');
cy.enter('#sample-app').then((getBody) => {
getBody().find('app-todo input.edit').should('be.visible');
@ -45,8 +45,8 @@ describe('edit properties of directive in the property view tab', () => {
beforeEach(() => {
// expand todo state
cy.get('.explorer-panel:contains("app-todo")')
.find('ng-property-view mat-tree-node:contains("todo")')
.click();
.find('ng-property-view mat-tree-node:contains("todo")')
.click();
});
it('should change todo label in app when edited', () => {
@ -57,13 +57,13 @@ describe('edit properties of directive in the property view tab', () => {
// find label variable and run through edit logic
cy.get('.explorer-panel:contains("app-todo")')
.find('ng-property-view mat-tree-node:contains("label")')
.find('ng-property-editor .editor')
.click()
.find('.editor-input')
.clear()
.type('Buy cookies')
.type('{enter}');
.find('ng-property-view mat-tree-node:contains("label")')
.find('ng-property-editor .editor')
.click()
.find('.editor-input')
.clear()
.type('Buy cookies')
.type('{enter}');
// assert that the page has been updated
cy.enter('#sample-app').then((getBody) => {
@ -79,13 +79,13 @@ describe('edit properties of directive in the property view tab', () => {
// find completed variable and run through edit logic
cy.get('.explorer-panel:contains("app-todo")')
.find('ng-property-view mat-tree-node:contains("completed")')
.find('ng-property-editor .editor')
.click()
.find('.editor-input')
.clear()
.type('true')
.type('{enter}');
.find('ng-property-view mat-tree-node:contains("completed")')
.find('ng-property-editor .editor')
.click()
.find('.editor-input')
.clear()
.type('true')
.type('{enter}');
// assert that the page has been updated
cy.enter('#sample-app').then((getBody) => {

View file

@ -21,20 +21,21 @@ describe('change of the state should reflect in property update', () => {
// Select the todo item
cy.get('.tree-wrapper')
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
.find('.tree-node:contains("app-todo[TooltipDirective]")')
.first()
.click({force: true});
// Expand the todo in the property explorer
cy.get('.explorer-panel:contains("app-todo")')
.find('ng-property-view mat-tree-node:contains("todo")')
.click();
.find('ng-property-view mat-tree-node:contains("todo")')
.click();
// Verify its value is now completed
cy.contains(
'.explorer-panel:contains("app-todo") ' +
'ng-property-view mat-tree-node:contains("completed") ' +
'ng-property-editor .editor',
'true');
'.explorer-panel:contains("app-todo") ' +
'ng-property-view mat-tree-node:contains("completed") ' +
'ng-property-editor .editor',
'true',
);
});
});

View file

@ -17,9 +17,9 @@ describe('Viewing component metadata', () => {
});
describe('viewing TodoComponent', () => {
beforeEach(
() => prepareHeaderExpansionPanelForAssertions(
'.tree-node:contains("app-todo[TooltipDirective]")'));
beforeEach(() =>
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'),
);
it('should display view encapsulation', () => {
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated');
@ -31,9 +31,9 @@ describe('Viewing component metadata', () => {
});
describe('viewing DemoAppComponent', () => {
beforeEach(
() =>
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'));
beforeEach(() =>
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'),
);
it('should display view encapsulation', () => {
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None');

View file

@ -17,5 +17,4 @@ import {AppComponent} from './app.component';
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {
}
export class AppModule {}

View file

@ -11,7 +11,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule, {
ngZone: 'noop',
})
.catch((err) => console.error(err));
.bootstrapModule(AppModule, {
ngZone: 'noop',
})
.catch((err) => console.error(err));

View file

@ -13,7 +13,7 @@ import {Router, RouterOutlet} from '@angular/router';
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
standalone: true,
imports: [RouterOutlet]
imports: [RouterOutlet],
})
export class AppComponent {
constructor(public router: Router) {}

View file

@ -7,7 +7,20 @@
*/
import {JsonPipe} from '@angular/common';
import {Component, computed, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, signal, ViewChild, ViewEncapsulation} from '@angular/core';
import {
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
EventEmitter,
inject,
Injector,
Input,
Output,
signal,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {createCustomElement} from '@angular/elements';
import {RouterOutlet} from '@angular/router';
import {initializeMessageBus} from 'ng-devtools-backend';
@ -24,7 +37,7 @@ import {ZippyComponent} from './zippy.component';
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [HeavyComponent, RouterOutlet, JsonPipe],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class DemoAppComponent {
@ViewChild(ZippyComponent) zippy!: ZippyComponent;
@ -50,7 +63,7 @@ export class DemoAppComponent {
customElements.define('app-zippy', el as any);
}
getTitle(): '► Click to expand'|'▼ Click to collapse' {
getTitle(): '► Click to expand' | '▼ Click to collapse' {
if (!this.zippy || !this.zippy.visible) {
return '► Click to expand';
}
@ -71,5 +84,10 @@ export const ROUTES = [
},
];
initializeMessageBus(new ZoneUnawareIFrameMessageBus(
'angular-devtools-backend', 'angular-devtools', () => window.parent));
initializeMessageBus(
new ZoneUnawareIFrameMessageBus(
'angular-devtools-backend',
'angular-devtools',
() => window.parent,
),
);

View file

@ -18,8 +18,7 @@ const fib = (n: number): number => {
@Component({selector: 'app-heavy', template: `<h1>{{ calculate() }}</h1>`, standalone: true})
export class HeavyComponent {
@Input()
set foo(_: any) {
}
set foo(_: any) {}
state = {
nested: {
@ -27,14 +26,12 @@ export class HeavyComponent {
foo: 1,
bar: 2,
},
[Symbol(3)]():
number {
return 1.618;
},
get foo():
number {
return 42;
},
[Symbol(3)](): number {
return 1.618;
},
get foo(): number {
return 42;
},
},
};
calculate(): number {

View file

@ -18,7 +18,6 @@ import {RouterLink, RouterOutlet} from '@angular/router';
<a [routerLink]="">Home</a>
<a [routerLink]="">Home</a>
<a [routerLink]="">Home</a>
`
`,
})
export class AboutComponent {
}
export class AboutComponent {}

View file

@ -12,7 +12,6 @@ import {FormsModule} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
export interface DialogData {
animal: string;
name: string;
@ -39,8 +38,9 @@ export interface DialogData {
})
export class DialogComponent {
constructor(
public dialogRef: MatDialogRef<DialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) {}
public dialogRef: MatDialogRef<DialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
) {}
onNoClick(): void {
this.dialogRef.close();

View file

@ -22,17 +22,21 @@ export interface Todo {
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TooltipDirective],
styles: [`
styles: [
`
.destroy {
cursor: pointer;
display: unset !important;
}
`],
`,
],
template: `
<li [class.completed]="todo.completed">
<div class="view" appTooltip>
<input class="toggle" type="checkbox" [checked]="todo.completed" (change)="toggle()" />
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{ todo.label }}</label>
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{
todo.label
}}</label>
<button class="destroy" (click)="delete.emit(todo)"></button>
</div>
<input
@ -42,7 +46,7 @@ export interface Todo {
(keydown.enter)="completeEdit($any($event.target).value)"
/>
</li>
`
`,
})
export class TodoComponent {
@Input() todo!: Todo;

View file

@ -7,7 +7,16 @@
*/
import {NgForOf} from '@angular/common';
import {ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, Pipe, PipeTransform} from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
OnDestroy,
OnInit,
Output,
Pipe,
PipeTransform,
} from '@angular/core';
import {RouterLink} from '@angular/router';
import {SamplePipe} from './sample.pipe';
@ -57,19 +66,25 @@ const fib = (n: number): number => {
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input (keydown.enter)="addTodo(input)" #input class="new-todo" placeholder="What needs to be done?" autofocus />
<input
(keydown.enter)="addTodo(input)"
#input
class="new-todo"
placeholder="What needs to be done?"
autofocus
/>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
@for (todo of todos | todosFilter: filterValue; track todo) {
<app-todo
appTooltip
[todo]="todo"
(delete)="onDelete($event)"
(update)="onChange($event)"
/>
<app-todo
appTooltip
[todo]="todo"
(delete)="onDelete($event)"
(update)="onChange($event)"
/>
}
</ul>
</section>
@ -80,7 +95,7 @@ const fib = (n: number): number => {
<button class="clear-completed" (click)="clearCompleted()">Clear completed</button>
</footer>
</section>
`
`,
})
export class TodosComponent implements OnInit, OnDestroy {
todos: Todo[] = [

View file

@ -19,9 +19,16 @@ import {DialogComponent} from './dialog.component';
@Component({
selector: 'app-todo-demo',
standalone: true,
imports:
[RouterLink, RouterOutlet, MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule],
styles: [`
imports: [
RouterLink,
RouterOutlet,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
FormsModule,
],
styles: [
`
nav {
padding-top: 20px;
padding-bottom: 10px;
@ -37,7 +44,8 @@ import {DialogComponent} from './dialog.component';
padding: 10px;
margin-right: 20px;
}
`],
`,
],
template: `
<nav>
<a routerLink="/demo-app/todos/app">Todos</a>
@ -47,7 +55,7 @@ import {DialogComponent} from './dialog.component';
<button class="dialog-open-button" (click)="openDialog()">Open dialog</button>
<router-outlet></router-outlet>
`
`,
})
export class TodoAppComponent {
name!: string;

View file

@ -11,27 +11,29 @@ import {Component, Input} from '@angular/core';
@Component({
selector: 'app-zippy',
standalone: true,
styles: [`
:host {
user-select: none;
}
styles: [
`
:host {
user-select: none;
}
header {
max-width: 120px;
border: 1px solid #ccc;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
}
header {
max-width: 120px;
border: 1px solid #ccc;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
}
div {
max-width: 120px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding: 10px;
}
`],
div {
max-width: 120px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding: 10px;
}
`,
],
template: `
<section>
<header (click)="visible = !visible">{{ title }}</header>
@ -39,7 +41,7 @@ import {Component, Input} from '@angular/core';
<ng-content></ng-content>
</div>
</section>
`
`,
})
export class ZippyComponent {
@Input() title!: string;

View file

@ -19,33 +19,39 @@ import {IFrameMessageBus} from '../../../../../src/iframe-message-bus';
{
provide: MessageBus,
useFactory(): MessageBus<Events> {
return new PriorityAwareMessageBus(new IFrameMessageBus(
'angular-devtools', 'angular-devtools-backend',
return new PriorityAwareMessageBus(
new IFrameMessageBus(
'angular-devtools',
'angular-devtools-backend',
// tslint:disable-next-line: no-non-null-assertion
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!));
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
),
);
},
},
],
styles: [`
iframe {
height: 340px;
width: 100%;
border: 0;
}
styles: [
`
iframe {
height: 340px;
width: 100%;
border: 0;
}
.devtools-wrapper {
height: calc(100vh - 345px);
}
`],
.devtools-wrapper {
height: calc(100vh - 345px);
}
`,
],
template: `
<iframe #ref src="demo-app/todos/app" id="sample-app"></iframe>
<br />
<div class="devtools-wrapper">
<ng-devtools></ng-devtools>
</div>
`
`,
})
export class DevToolsComponent {
messageBus: IFrameMessageBus|null = null;
messageBus: IFrameMessageBus | null = null;
@ViewChild('ref') iframe!: ElementRef;
}

View file

@ -8,5 +8,5 @@
export const environment = {
production: false,
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
};

View file

@ -16,7 +16,6 @@ import {DemoApplicationOperations} from '../../../src/demo-application-operation
import {AppComponent} from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideAnimations(),
@ -24,7 +23,7 @@ bootstrapApplication(AppComponent, {
{
path: '',
loadComponent: () =>
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
pathMatch: 'full',
},
{
@ -40,5 +39,5 @@ bootstrapApplication(AppComponent, {
provide: ApplicationEnvironment,
useClass: DemoApplicationEnvironment,
},
]
],
});

View file

@ -6,12 +6,43 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentExplorerViewQuery, ComponentType, DevToolsNode, DirectivePosition, DirectiveType, ElementPosition, Events, MessageBus, ProfilerFrame, SerializedInjector, SerializedProviderRecord} from 'protocol';
import {
ComponentExplorerViewQuery,
ComponentType,
DevToolsNode,
DirectivePosition,
DirectiveType,
ElementPosition,
Events,
MessageBus,
ProfilerFrame,
SerializedInjector,
SerializedProviderRecord,
} from 'protocol';
import {debounceTime} from 'rxjs/operators';
import {appIsAngularInDevMode, appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion,} from 'shared-utils';
import {
appIsAngularInDevMode,
appIsAngularIvy,
appIsSupportedAngularVersion,
getAngularVersion,
} from 'shared-utils';
import {ComponentInspector} from './component-inspector/component-inspector';
import {getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, idToInjector, injectorsSeen, isElementInjector, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState} from './component-tree';
import {
getElementInjectorElement,
getInjectorFromElementNode,
getInjectorProviders,
getInjectorResolutionPath,
getLatestComponentState,
idToInjector,
injectorsSeen,
isElementInjector,
nodeInjectorToResolutionPath,
queryDirectiveForest,
serializeProviderRecord,
serializeResolutionPath,
updateState,
} from './component-tree';
import {unHighlight} from './highlighter';
import {disableTimingAPI, enableTimingAPI, initializeOrGetDirectiveForestHooks} from './hooks';
import {start as startProfiling, stop as stopProfiling} from './hooks/capture';
@ -25,7 +56,9 @@ export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void =>
messageBus.on('shutdown', shutdownCallback(messageBus));
messageBus.on(
'getLatestComponentExplorerView', getLatestComponentExplorerViewCallback(messageBus));
'getLatestComponentExplorerView',
getLatestComponentExplorerViewCallback(messageBus),
);
messageBus.on('queryNgAvailability', checkForAngularCallback(messageBus));
@ -54,8 +87,8 @@ export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void =>
// once every 250ms
runOutsideAngular(() => {
initializeOrGetDirectiveForestHooks()
.profiler.changeDetection$.pipe(debounceTime(250))
.subscribe(() => messageBus.emit('componentTreeDirty'));
.profiler.changeDetection$.pipe(debounceTime(250))
.subscribe(() => messageBus.emit('componentTreeDirty'));
});
}
};
@ -68,55 +101,58 @@ const shutdownCallback = (messageBus: MessageBus<Events>) => () => {
messageBus.destroy();
};
const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>) =>
(query?: ComponentExplorerViewQuery) => {
// We want to force re-indexing of the component tree.
// Pressing the refresh button means the user saw stuck UI.
const getLatestComponentExplorerViewCallback =
(messageBus: MessageBus<Events>) => (query?: ComponentExplorerViewQuery) => {
// We want to force re-indexing of the component tree.
// Pressing the refresh button means the user saw stuck UI.
initializeOrGetDirectiveForestHooks().indexForest();
initializeOrGetDirectiveForestHooks().indexForest();
const forest = prepareForestForSerialization(
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
ngDebugDependencyInjectionApiIsSupported());
const forest = prepareForestForSerialization(
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
ngDebugDependencyInjectionApiIsSupported(),
);
// cleanup injector id mappings
for (const injectorId of idToInjector.keys()) {
if (!injectorsSeen.has(injectorId)) {
const injector = idToInjector.get(injectorId)!;
if (isElementInjector(injector)) {
const element = getElementInjectorElement(injector);
if (element) {
nodeInjectorToResolutionPath.delete(element);
}
// cleanup injector id mappings
for (const injectorId of idToInjector.keys()) {
if (!injectorsSeen.has(injectorId)) {
const injector = idToInjector.get(injectorId)!;
if (isElementInjector(injector)) {
const element = getElementInjectorElement(injector);
if (element) {
nodeInjectorToResolutionPath.delete(element);
}
idToInjector.delete(injectorId);
}
}
injectorsSeen.clear();
if (!query) {
messageBus.emit('latestComponentExplorerView', [{forest}]);
return;
idToInjector.delete(injectorId);
}
}
injectorsSeen.clear();
const state = getLatestComponentState(
query, initializeOrGetDirectiveForestHooks().getDirectiveForest());
if (!query) {
messageBus.emit('latestComponentExplorerView', [{forest}]);
return;
}
if (state) {
const {directiveProperties} = state;
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
}
};
const state = getLatestComponentState(
query,
initializeOrGetDirectiveForestHooks().getDirectiveForest(),
);
if (state) {
const {directiveProperties} = state;
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
}
};
const checkForAngularCallback = (messageBus: MessageBus<Events>) => () =>
checkForAngular(messageBus);
checkForAngular(messageBus);
const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(messageBus);
const startProfilingCallback = (messageBus: MessageBus<Events>) => () =>
startProfiling((frame: ProfilerFrame) => {
messageBus.emit('sendProfilerChunk', [frame]);
});
startProfiling((frame: ProfilerFrame) => {
messageBus.emit('sendProfilerChunk', [frame]);
});
const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
messageBus.emit('profilerResults', [stopProfiling()]);
@ -124,33 +160,41 @@ const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
const selectedComponentCallback = (position: ElementPosition) => {
const node = queryDirectiveForest(
position, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
position,
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
);
setConsoleReference({node, position});
};
const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (
position: DirectivePosition, propPath: string[]) => {
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
const node = queryDirectiveForest(
position.element, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
if (!node) {
return emitEmpty();
}
const current =
position.directive === undefined ? node.component : node.directives[position.directive];
if (!current) {
return emitEmpty();
}
let data = current.instance;
for (const prop of propPath) {
data = data[prop];
if (!data) {
console.error('Cannot access the properties', propPath, 'of', node);
const getNestedPropertiesCallback =
(messageBus: MessageBus<Events>) => (position: DirectivePosition, propPath: string[]) => {
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
const node = queryDirectiveForest(
position.element,
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
);
if (!node) {
return emitEmpty();
}
}
messageBus.emit('nestedProperties', [position, {props: serializeDirectiveState(data)}, propPath]);
return;
};
const current =
position.directive === undefined ? node.component : node.directives[position.directive];
if (!current) {
return emitEmpty();
}
let data = current.instance;
for (const prop of propPath) {
data = data[prop];
if (!data) {
console.error('Cannot access the properties', propPath, 'of', node);
}
}
messageBus.emit('nestedProperties', [
position,
{props: serializeDirectiveState(data)},
propPath,
]);
return;
};
//
// Subscribe Helpers
@ -209,44 +253,46 @@ export interface SerializableComponentInstanceType extends ComponentType {
id: number;
}
export interface SerializableComponentTreeNode extends
DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
export interface SerializableComponentTreeNode
extends DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
children: SerializableComponentTreeNode[];
}
// Here we drop properties to prepare the tree for serialization.
// We don't need the component instance, so we just traverse the tree
// and leave the component name.
const prepareForestForSerialization = (roots: ComponentTreeNode[], includeResolutionPath = false):
SerializableComponentTreeNode[] => {
const serializedNodes: SerializableComponentTreeNode[] = [];
for (const node of roots) {
const serializedNode: SerializableComponentTreeNode = {
element: node.element,
component: node.component ? {
const prepareForestForSerialization = (
roots: ComponentTreeNode[],
includeResolutionPath = false,
): SerializableComponentTreeNode[] => {
const serializedNodes: SerializableComponentTreeNode[] = [];
for (const node of roots) {
const serializedNode: SerializableComponentTreeNode = {
element: node.element,
component: node.component
? {
name: node.component.name,
isElement: node.component.isElement,
id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance)!,
} :
null,
directives: node.directives.map(
(d) => ({
name: d.name,
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
})),
children: prepareForestForSerialization(node.children, includeResolutionPath),
};
serializedNodes.push(serializedNode);
if (includeResolutionPath) {
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
}
}
return serializedNodes;
}
: null,
directives: node.directives.map((d) => ({
name: d.name,
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
})),
children: prepareForestForSerialization(node.children, includeResolutionPath),
};
serializedNodes.push(serializedNode);
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|undefined {
if (includeResolutionPath) {
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
}
}
return serializedNodes;
};
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[] | undefined {
const nodeInjector = getInjectorFromElementNode(node.nativeElement!);
if (!nodeInjector) {
return [];
@ -274,91 +320,95 @@ function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|
return serializedPath;
}
const getInjectorProvidersCallback = (messageBus: MessageBus<Events>) =>
(injector: SerializedInjector) => {
if (!idToInjector.has(injector.id)) {
return;
const getInjectorProvidersCallback =
(messageBus: MessageBus<Events>) => (injector: SerializedInjector) => {
if (!idToInjector.has(injector.id)) {
return;
}
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
const allProviderRecords: SerializedProviderRecord[] = [];
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
for (const [index, providerRecord] of providerRecords.entries()) {
const record = serializeProviderRecord(
providerRecord,
index,
injector.type === 'environment',
);
allProviderRecords.push(record);
const records = tokenToRecords.get(providerRecord.token) ?? [];
records.push(record);
tokenToRecords.set(providerRecord.token, records);
}
const serializedProviderRecords: SerializedProviderRecord[] = [];
for (const [token, records] of tokenToRecords.entries()) {
const multiRecords = records.filter((record) => record.multi);
const nonMultiRecords = records.filter((record) => !record.multi);
for (const record of nonMultiRecords) {
serializedProviderRecords.push(record);
}
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
const allProviderRecords: SerializedProviderRecord[] = [];
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
for (const [index, providerRecord] of providerRecords.entries()) {
const record =
serializeProviderRecord(providerRecord, index, injector.type === 'environment');
allProviderRecords.push(record);
const records = tokenToRecords.get(providerRecord.token) ?? [];
records.push(record);
tokenToRecords.set(providerRecord.token, records);
const [firstMultiRecord] = multiRecords;
if (firstMultiRecord !== undefined) {
// All multi providers will have the same token, so we can just use the first one.
serializedProviderRecords.push({
token: firstMultiRecord.token,
type: 'multi',
multi: true,
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
// provided as viewProviders
isViewProvider: firstMultiRecord.isViewProvider,
index: records.map((record) => record.index as number),
});
}
}
const serializedProviderRecords: SerializedProviderRecord[] = [];
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
};
for (const [token, records] of tokenToRecords.entries()) {
const multiRecords = records.filter(record => record.multi);
const nonMultiRecords = records.filter(record => !record.multi);
const logProvider = (
serializedInjector: SerializedInjector,
serializedProvider: SerializedProviderRecord,
): void => {
if (!idToInjector.has(serializedInjector.id)) {
return;
}
for (const record of nonMultiRecords) {
serializedProviderRecords.push(record);
}
const injector = idToInjector.get(serializedInjector.id)!;
const [firstMultiRecord] = multiRecords;
if (firstMultiRecord !== undefined) {
// All multi providers will have the same token, so we can just use the first one.
serializedProviderRecords.push({
token: firstMultiRecord.token,
type: 'multi',
multi: true,
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
// provided as viewProviders
isViewProvider: firstMultiRecord.isViewProvider,
index: records.map(record => record.index as number),
});
}
}
const providerRecords = getInjectorProviders(injector);
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
};
console.group(
`%c${serializedInjector.name}`,
`color: ${
serializedInjector.type === 'element' ? '#a7d5a9' : '#f05057'
}; font-size: 1.25rem; font-weight: bold;`,
);
// tslint:disable-next-line:no-console
console.log('injector: ', injector);
const logProvider =
(serializedInjector: SerializedInjector, serializedProvider: SerializedProviderRecord):
void => {
if (!idToInjector.has(serializedInjector.id)) {
return;
}
if (typeof serializedProvider.index === 'number') {
const provider = providerRecords[serializedProvider.index];
const injector = idToInjector.get(serializedInjector.id)!;
// tslint:disable-next-line:no-console
console.log('provider: ', provider);
// tslint:disable-next-line:no-console
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
} else if (Array.isArray(serializedProvider.index)) {
const providers = serializedProvider.index.map((index) => providerRecords[index]);
const providerRecords = getInjectorProviders(injector);
// tslint:disable-next-line:no-console
console.log('providers: ', providers);
// tslint:disable-next-line:no-console
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
}
console.group(
`%c${serializedInjector.name}`,
`color: ${
serializedInjector.type === 'element' ?
'#a7d5a9' :
'#f05057'}; font-size: 1.25rem; font-weight: bold;`);
// tslint:disable-next-line:no-console
console.log('injector: ', injector);
if (typeof serializedProvider.index === 'number') {
const provider = providerRecords[serializedProvider.index];
// tslint:disable-next-line:no-console
console.log('provider: ', provider);
// tslint:disable-next-line:no-console
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
} else if (Array.isArray(serializedProvider.index)) {
const providers = serializedProvider.index.map(index => providerRecords[index]);
// tslint:disable-next-line:no-console
console.log('providers: ', providers);
// tslint:disable-next-line:no-console
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
}
console.groupEnd();
};
console.groupEnd();
};

View file

@ -14,7 +14,7 @@ import {initializeOrGetDirectiveForestHooks} from '../hooks';
import {ComponentTreeNode} from '../interfaces';
interface Type<T> extends Function {
new(...args: any[]): T;
new (...args: any[]): T;
}
export interface ComponentInspectorOptions {
onComponentEnter: (id: number) => void;
@ -28,11 +28,13 @@ export class ComponentInspector {
private readonly _onComponentSelect;
private readonly _onComponentLeave;
constructor(componentOptions: ComponentInspectorOptions = {
onComponentEnter: () => {},
onComponentLeave: () => {},
onComponentSelect: () => {},
}) {
constructor(
componentOptions: ComponentInspectorOptions = {
onComponentEnter: () => {},
onComponentLeave: () => {},
onComponentSelect: () => {},
},
) {
this.bindMethods();
this._onComponentEnter = componentOptions.onComponentEnter;
this._onComponentSelect = componentOptions.onComponentSelect;
@ -57,7 +59,8 @@ export class ComponentInspector {
if (this._selectedComponent.component && this._selectedComponent.host) {
this._onComponentSelect(
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
);
}
}
@ -73,7 +76,8 @@ export class ComponentInspector {
if (this._selectedComponent.component && this._selectedComponent.host) {
highlight(this._selectedComponent.host);
this._onComponentEnter(
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
);
}
}
@ -93,7 +97,7 @@ export class ComponentInspector {
highlightByPosition(position: ElementPosition): void {
const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest();
const elementToHighlight: HTMLElement|null = findNodeInForest(position, forest);
const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest);
if (elementToHighlight) {
highlight(elementToHighlight);
}

View file

@ -5,11 +5,28 @@
* 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 {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, PropertyQueryTypes, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol';
import {
ComponentExplorerViewQuery,
DirectiveMetadata,
DirectivesProperties,
ElementPosition,
PropertyQueryTypes,
SerializedInjectedService,
SerializedInjector,
SerializedProviderRecord,
UpdatedStateData,
} from 'protocol';
import {buildDirectiveTree, getLViewFromDirectiveOrElementInstance} from './directive-forest/index';
import {ngDebugApiIsSupported, ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
import {deeplySerializeSelectedProperties, serializeDirectiveState,} from './state-serializer/state-serializer';
import {
ngDebugApiIsSupported,
ngDebugClient,
ngDebugDependencyInjectionApiIsSupported,
} from './ng-debug-api/ng-debug-api';
import {
deeplySerializeSelectedProperties,
serializeDirectiveState,
} from './state-serializer/state-serializer';
// Need to be kept in sync with Angular framework
// We can't directly import it from framework now
@ -22,10 +39,21 @@ enum ChangeDetectionStrategy {
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces';
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core';
import type {
ClassProvider,
ExistingProvider,
FactoryProvider,
InjectOptions,
InjectionToken,
Injector,
Type,
ValueProvider,
ɵComponentDebugMetadata as ComponentDebugMetadata,
ɵProviderRecord as ProviderRecord,
} from '@angular/core';
import {isSignal} from './utils';
export const injectorToId = new WeakMap<Injector|HTMLElement, string>();
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
export const idToInjector = new Map<string, Injector>();
export const injectorsSeen = new Set<string>();
@ -47,12 +75,14 @@ export function getInjectorResolutionPath(injector: Injector): Injector[] {
return ngDebugClient().ɵgetInjectorResolutionPath(injector);
}
export function getInjectorFromElementNode(element: Node): Injector|null {
export function getInjectorFromElementNode(element: Node): Injector | null {
return ngDebugClient().getInjector(element);
}
function getDirectivesFromElement(element: HTMLElement):
{component: unknown|null; directives: unknown[];} {
function getDirectivesFromElement(element: HTMLElement): {
component: unknown | null;
directives: unknown[];
} {
let component = null;
if (element instanceof Element) {
component = ngDebugClient().getComponent(element);
@ -65,9 +95,9 @@ function getDirectivesFromElement(element: HTMLElement):
}
export const getLatestComponentState = (
query: ComponentExplorerViewQuery,
directiveForest?: ComponentTreeNode[],
): {directiveProperties: DirectivesProperties}|undefined => {
query: ComponentExplorerViewQuery,
directiveForest?: ComponentTreeNode[],
): {directiveProperties: DirectivesProperties} | undefined => {
// if a directive forest is passed in we don't have to build the forest again.
directiveForest = directiveForest ?? buildDirectiveForest();
@ -81,19 +111,19 @@ export const getLatestComponentState = (
const injector = ngDebugClient().getInjector(node.nativeElement!);
const injectors = getInjectorResolutionPath(injector);
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported() ?
[] :
injectors.map((injector) => ({
injector,
providers: getInjectorProviders(injector),
}));
const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported()
? []
: injectors.map((injector) => ({
injector,
providers: getInjectorProviders(injector),
}));
const populateResultSet = (dir: DirectiveInstanceType | ComponentInstanceType) => {
const {instance, name} = dir;
const metadata = getDirectiveMetadata(instance);
metadata.dependencies = getDependenciesForDirective(
injector,
resolutionPathWithProviders,
instance.constructor,
injector,
resolutionPathWithProviders,
instance.constructor,
);
if (query.propertyQuery.type === PropertyQueryTypes.All) {
@ -106,9 +136,9 @@ export const getLatestComponentState = (
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
directiveProperties[name] = {
props: deeplySerializeSelectedProperties(
instance,
query.propertyQuery.properties[name] || [],
),
instance,
query.propertyQuery.properties[name] || [],
),
metadata,
};
}
@ -124,7 +154,7 @@ export const getLatestComponentState = (
};
};
function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
function serializeElementInjectorWithId(injector: Injector): SerializedInjector | null {
let id: string;
const element = getElementInjectorElement(injector);
@ -146,7 +176,7 @@ function serializeElementInjectorWithId(injector: Injector): SerializedInjector|
return {id, ...serializedInjector};
}
function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
function serializeInjectorWithId(injector: Injector): SerializedInjector | null {
if (isElementInjector(injector)) {
return serializeElementInjectorWithId(injector);
} else {
@ -154,7 +184,7 @@ function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
}
}
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null {
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector | null {
let id: string;
if (!injectorToId.has(injector)) {
@ -224,16 +254,16 @@ export function getInjectorProviders(injector: Injector) {
}
const getDependenciesForDirective = (
injector: Injector,
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
directive: any,
): SerializedInjectedService[] => {
injector: Injector,
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
directive: any,
): SerializedInjectedService[] => {
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
return [];
}
let dependencies =
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
const serializedInjectedServices: SerializedInjectedService[] = [];
let position = 0;
@ -256,8 +286,9 @@ const getDependenciesForDirective = (
// dependency (2)
const dependencyResolutionPath: SerializedInjector[] = [
// (1)
...resolutionPath.slice(0, foundInjectorIndex + 1)
.map((node) => serializeInjectorWithId(node.injector)!),
...resolutionPath
.slice(0, foundInjectorIndex + 1)
.map((node) => serializeInjectorWithId(node.injector)!),
// (2)
// We slice the import path to remove the first element because this is the same
@ -314,7 +345,7 @@ function stripUnderscore(str: string): string {
return str;
}
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'>|null {
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'> | null {
const metadata = getInjectorMetadata(injector);
if (metadata === null) {
@ -354,11 +385,11 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
}
export function serializeProviderRecord(
providerRecord: ProviderRecord,
index: number,
hasImportPath = false,
): SerializedProviderRecord {
let type: 'type'|'class'|'value'|'factory'|'existing' = 'type';
providerRecord: ProviderRecord,
index: number,
hasImportPath = false,
): SerializedProviderRecord {
let type: 'type' | 'class' | 'value' | 'factory' | 'existing' = 'type';
let multi = false;
if (typeof providerRecord.provider === 'object') {
@ -378,7 +409,11 @@ export function serializeProviderRecord(
}
const serializedProvider: {
token: string; type: typeof type; multi: boolean; isViewProvider: boolean; index: number;
token: string;
type: typeof type;
multi: boolean;
isViewProvider: boolean;
index: number;
importPath?: string[];
} = {
token: valueToLabel(providerRecord.token),
@ -389,10 +424,9 @@ export function serializeProviderRecord(
};
if (hasImportPath) {
serializedProvider['importPath'] = (providerRecord.importPath ?? [])
.map(
(injector) => valueToLabel(injector),
);
serializedProvider['importPath'] = (providerRecord.importPath ?? []).map((injector) =>
valueToLabel(injector),
);
}
return serializedProvider;
@ -401,8 +435,8 @@ export function serializeProviderRecord(
function elementToDirectiveNames(element: HTMLElement): string[] {
const {component, directives} = getDirectivesFromElement(element);
return [component, ...directives]
.map((dir) => dir?.constructor?.name ?? '')
.filter((dir) => !!dir);
.map((dir) => dir?.constructor?.name ?? '')
.filter((dir) => !!dir);
}
export function getElementInjectorElement(elementInjector: Injector): HTMLElement {
@ -413,7 +447,7 @@ export function getElementInjectorElement(elementInjector: Injector): HTMLElemen
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
}
function isInjectionToken(token: Type<unknown>|InjectionToken<unknown>): boolean {
function isInjectionToken(token: Type<unknown> | InjectionToken<unknown>): boolean {
return token.constructor.name === 'InjectionToken';
}
@ -447,7 +481,7 @@ const getRoots = () => {
const roots = Array.from(document.documentElement.querySelectorAll('[ng-version]'));
const isTopLevel = (element: Element) => {
let parent: Element|null = element;
let parent: Element | null = element;
while (parent?.parentElement) {
parent = parent.parentElement;
@ -470,13 +504,13 @@ export const buildDirectiveForest = (): ComponentTreeNode[] => {
// Based on an ElementID we return a specific component node.
// If we can't find any, we return null.
export const queryDirectiveForest = (
position: ElementPosition,
forest: ComponentTreeNode[],
): ComponentTreeNode|null => {
position: ElementPosition,
forest: ComponentTreeNode[],
): ComponentTreeNode | null => {
if (!position.length) {
return null;
}
let node: null|ComponentTreeNode = null;
let node: null | ComponentTreeNode = null;
for (const i of position) {
node = forest[i];
if (!node) {
@ -488,16 +522,16 @@ export const queryDirectiveForest = (
};
export const findNodeInForest = (
position: ElementPosition,
forest: ComponentTreeNode[],
): HTMLElement|null => {
const foundComponent: ComponentTreeNode|null = queryDirectiveForest(position, forest);
position: ElementPosition,
forest: ComponentTreeNode[],
): HTMLElement | null => {
const foundComponent: ComponentTreeNode | null = queryDirectiveForest(position, forest);
return foundComponent ? (foundComponent.nativeElement as HTMLElement) : null;
};
export const findNodeFromSerializedPosition = (
serializedPosition: string,
): ComponentTreeNode|null => {
serializedPosition: string,
): ComponentTreeNode | null => {
const position: number[] = serializedPosition.split(',').map((index) => parseInt(index, 10));
return queryDirectiveForest(position, buildDirectiveForest());
};
@ -507,9 +541,9 @@ export const updateState = (updatedStateData: UpdatedStateData): void => {
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest());
if (!node) {
console.warn(
'Could not update the state of component',
updatedStateData,
'because the component was not found',
'Could not update the state of component',
updatedStateData,
'because the component was not found',
);
return;
}
@ -551,15 +585,14 @@ const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDi
} else {
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
}
} catch {
}
} catch {}
};
export function serializeResolutionPath(resolutionPath: Injector[]): SerializedInjector[] {
const serializedResolutionPath: SerializedInjector[] = [];
for (const injector of resolutionPath) {
let serializedInjectorWithId: SerializedInjector|null = null;
let serializedInjectorWithId: SerializedInjector | null = null;
if (isElementInjector(injector)) {
serializedInjectorWithId = serializeElementInjectorWithId(injector);

View file

@ -9,12 +9,16 @@
import {LTreeStrategy} from './ltree';
import {RTreeStrategy} from './render-tree';
export {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME} from './ltree';
export {
getDirectiveHostElement,
getLViewFromDirectiveOrElementInstance,
METADATA_PROPERTY_NAME,
} from './ltree';
// The order of the strategies matters. Lower indices have higher priority.
const strategies = [new RTreeStrategy(), new LTreeStrategy()];
let strategy: null|RTreeStrategy|LTreeStrategy = null;
let strategy: null | RTreeStrategy | LTreeStrategy = null;
const selectStrategy = (element: Element) => {
for (const s of strategies) {

View file

@ -28,7 +28,6 @@ const TYPE = 1;
const ELEMENT = 0;
const LVIEW_TVIEW = 1;
// Big oversimplification of the LView structure.
type LView = Array<any>;
@ -41,7 +40,7 @@ const isLView = (value: unknown): value is LView => {
};
export const METADATA_PROPERTY_NAME = '__ngContext__';
export function getLViewFromDirectiveOrElementInstance(dir: any): null|LView {
export function getLViewFromDirectiveOrElementInstance(dir: any): null | LView {
if (!dir) {
return null;
}
@ -80,7 +79,7 @@ export class LTreeStrategy {
private _getNode(lView: LView, data: any, idx: number): ComponentTreeNode {
const directives: DirectiveInstanceType[] = [];
let component: ComponentInstanceType|null = null;
let component: ComponentInstanceType | null = null;
const tNode = data[idx];
const node = lView[idx][ELEMENT];
const element = (node.tagName || node.nodeName).toLowerCase();

View file

@ -19,7 +19,7 @@ describe('render tree extraction', () => {
componentMap = new Map();
(window as any).ng = {
getDirectiveMetadata(): void{},
getDirectiveMetadata(): void {},
getComponent(element: Element): any {
return componentMap.get(element);
},

View file

@ -10,56 +10,60 @@ import {ComponentTreeNode} from '../interfaces';
import {ngDebugClient} from '../ng-debug-api/ng-debug-api';
import {isCustomElement} from '../utils';
const extractViewTree =
(domNode: Node|Element, result: ComponentTreeNode[],
getComponent: (element: Element) => {} | null,
getDirectives: (node: Node) => {}[]): ComponentTreeNode[] => {
const directives = getDirectives(domNode);
if (!directives.length && !(domNode instanceof Element)) {
return result;
}
const componentTreeNode: ComponentTreeNode = {
children: [],
component: null,
directives: directives.map((dir) => {
return {
instance: dir,
name: dir.constructor.name,
};
}),
element: domNode.nodeName.toLowerCase(),
nativeElement: domNode,
const extractViewTree = (
domNode: Node | Element,
result: ComponentTreeNode[],
getComponent: (element: Element) => {} | null,
getDirectives: (node: Node) => {}[],
): ComponentTreeNode[] => {
const directives = getDirectives(domNode);
if (!directives.length && !(domNode instanceof Element)) {
return result;
}
const componentTreeNode: ComponentTreeNode = {
children: [],
component: null,
directives: directives.map((dir) => {
return {
instance: dir,
name: dir.constructor.name,
};
if (!(domNode instanceof Element)) {
result.push(componentTreeNode);
return result;
}
const component = getComponent(domNode);
if (component) {
componentTreeNode.component = {
instance: component,
isElement: isCustomElement(domNode),
name: domNode.nodeName.toLowerCase(),
};
}
if (component || componentTreeNode.directives.length) {
result.push(componentTreeNode);
}
if (componentTreeNode.component || componentTreeNode.directives.length) {
domNode.childNodes.forEach(
(node) =>
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives));
} else {
domNode.childNodes.forEach(
(node) => extractViewTree(node, result, getComponent, getDirectives));
}
return result;
}),
element: domNode.nodeName.toLowerCase(),
nativeElement: domNode,
};
if (!(domNode instanceof Element)) {
result.push(componentTreeNode);
return result;
}
const component = getComponent(domNode);
if (component) {
componentTreeNode.component = {
instance: component,
isElement: isCustomElement(domNode),
name: domNode.nodeName.toLowerCase(),
};
}
if (component || componentTreeNode.directives.length) {
result.push(componentTreeNode);
}
if (componentTreeNode.component || componentTreeNode.directives.length) {
domNode.childNodes.forEach((node) =>
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives),
);
} else {
domNode.childNodes.forEach((node) =>
extractViewTree(node, result, getComponent, getDirectives),
);
}
return result;
};
export class RTreeStrategy {
supports(): boolean {
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const)
.every((method) => typeof ngDebugClient()[method] === 'function');
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const).every(
(method) => typeof ngDebugClient()[method] === 'function',
);
}
build(element: Element): ComponentTreeNode[] {

View file

@ -12,7 +12,7 @@ let overlayContent: HTMLElement;
declare const ng: any;
interface Type<T> extends Function {
new(...args: any[]): T;
new (...args: any[]): T;
}
const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight';
@ -40,26 +40,27 @@ function init(): void {
overlay.appendChild(overlayContent);
}
export const findComponentAndHost =
(el: Node|undefined): {component: any; host: HTMLElement | null} => {
if (!el) {
return {component: null, host: null};
}
while (el) {
const component = el instanceof HTMLElement && ng.getComponent(el);
if (component) {
return {component, host: el as HTMLElement};
}
if (!el.parentElement) {
break;
}
el = el.parentElement;
}
return {component: null, host: null};
};
export const findComponentAndHost = (
el: Node | undefined,
): {component: any; host: HTMLElement | null} => {
if (!el) {
return {component: null, host: null};
}
while (el) {
const component = el instanceof HTMLElement && ng.getComponent(el);
if (component) {
return {component, host: el as HTMLElement};
}
if (!el.parentElement) {
break;
}
el = el.parentElement;
}
return {component: null, host: null};
};
// Todo(aleksanderbodurri): this should not be part of the highlighter, move this somewhere else
export function getDirectiveName(dir: Type<unknown>|undefined|null): string {
export function getDirectiveName(dir: Type<unknown> | undefined | null): string {
return dir ? dir.constructor.name : 'unknown';
}
@ -97,11 +98,12 @@ export function inDoc(node: any): boolean {
}
const doc = node.ownerDocument.documentElement;
const parent = node.parentNode;
return doc === node || doc === parent ||
!!(parent && parent.nodeType === 1 && doc.contains(parent));
return (
doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent))
);
}
function getComponentRect(el: Node): DOMRect|undefined {
function getComponentRect(el: Node): DOMRect | undefined {
if (!(el instanceof HTMLElement)) {
return;
}
@ -137,7 +139,7 @@ function positionOverlayContent(dimensions: DOMRect) {
// Attempt to position the content element so that it's always in the
// viewport along the Y axis. Prefer to position on the bottom.
if ((dimensions.bottom + yOffset) <= viewportHeight) {
if (dimensions.bottom + yOffset <= viewportHeight) {
style.bottom = yOffsetValue;
// If it doesn't fit on the bottom, try to position on top.
} else if (dimensions.top - yOffset >= 0) {

View file

@ -6,7 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {DirectiveProfile, ElementPosition, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
import {
DirectiveProfile,
ElementPosition,
ElementProfile,
LifecycleProfile,
ProfilerFrame,
} from 'protocol';
import {getDirectiveName} from '../highlighter';
import {ComponentTreeNode} from '../interfaces';
@ -59,16 +65,20 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
// We flush here because it's possible the current node to overwrite
// an existing removed node.
onCreate(
directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition):
void {
eventMap.set(directive, {
isElement: isCustomElement(node),
name: getDirectiveName(directive),
isComponent,
lifecycle: {},
outputs: {},
});
},
directive: any,
node: Node,
_: number,
isComponent: boolean,
position: ElementPosition,
): void {
eventMap.set(directive, {
isElement: isCustomElement(node),
name: getDirectiveName(directive),
isComponent,
lifecycle: {},
outputs: {},
});
},
onChangeDetectionStart(component: any, node: Node): void {
startEvent(timeStartMap, component, 'changeDetection');
if (!inChangeDetection) {
@ -111,22 +121,31 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
console.warn('Could not find profile for', component);
}
},
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition):
void {
// Make sure we reflect such directives in the report.
if (!eventMap.has(directive)) {
eventMap.set(directive, {
isElement: isComponent && isCustomElement(node),
name: getDirectiveName(directive),
isComponent,
lifecycle: {},
outputs: {},
});
}
},
onDestroy(
directive: any,
node: Node,
_: number,
isComponent: boolean,
__: ElementPosition,
): void {
// Make sure we reflect such directives in the report.
if (!eventMap.has(directive)) {
eventMap.set(directive, {
isElement: isComponent && isCustomElement(node),
name: getDirectiveName(directive),
isComponent,
lifecycle: {},
outputs: {},
});
}
},
onLifecycleHookStart(
directive: any, hookName: keyof LifecycleProfile, node: Node, __: number,
isComponent: boolean): void {
directive: any,
hookName: keyof LifecycleProfile,
node: Node,
__: number,
isComponent: boolean,
): void {
startEvent(timeStartMap, directive, hookName);
if (!eventMap.has(directive)) {
eventMap.set(directive, {
@ -139,7 +158,12 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
}
},
onLifecycleHookEnd(
directive: any, hookName: keyof LifecycleProfile, _: Node, __: number, ___: boolean): void {
directive: any,
hookName: keyof LifecycleProfile,
_: Node,
__: number,
___: boolean,
): void {
const dir = eventMap.get(directive);
const startTimestamp = getEventStart(timeStartMap, directive, hookName);
if (startTimestamp === undefined) {
@ -153,19 +177,23 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
dir.lifecycle[hookName] = (dir.lifecycle[hookName] || 0) + duration;
frameDuration += duration;
},
onOutputStart(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean):
void {
startEvent(timeStartMap, componentOrDirective, outputName);
if (!eventMap.has(componentOrDirective)) {
eventMap.set(componentOrDirective, {
isElement: isCustomElement(node),
name: getDirectiveName(componentOrDirective),
isComponent,
lifecycle: {},
outputs: {},
});
}
},
onOutputStart(
componentOrDirective: any,
outputName: string,
node: Node,
isComponent: boolean,
): void {
startEvent(timeStartMap, componentOrDirective, outputName);
if (!eventMap.has(componentOrDirective)) {
eventMap.set(componentOrDirective, {
isElement: isCustomElement(node),
name: getDirectiveName(componentOrDirective),
isComponent,
lifecycle: {},
outputs: {},
});
}
},
onOutputEnd(componentOrDirective: any, outputName: string): void {
const name = outputName;
const entry = eventMap.get(componentOrDirective);
@ -175,8 +203,10 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
}
if (!entry) {
console.warn(
'Could not find directive or component in onOutputEnd callback', componentOrDirective,
outputName);
'Could not find directive or component in onOutputEnd callback',
componentOrDirective,
outputName,
);
return;
}
const duration = performance.now() - startTimestamp;
@ -215,33 +245,36 @@ const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) =>
}
};
const insertElementProfile =
(frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
if (!profile) {
return;
}
const original = frames;
for (let i = 0; i < position.length - 1; i++) {
const pos = position[i];
if (!frames[pos]) {
// TODO(mgechev): consider how to ensure we don't hit this case
console.warn('Unable to find parent node for', profile, original);
return;
}
frames = frames[pos].children;
}
const lastIdx = position[position.length - 1];
let lastFrame: ElementProfile = {
children: [],
directives: [],
};
if (frames[lastIdx]) {
lastFrame = frames[lastIdx];
} else {
frames[lastIdx] = lastFrame;
}
insertOrMerge(lastFrame, profile);
};
const insertElementProfile = (
frames: ElementProfile[],
position: ElementPosition,
profile?: DirectiveProfile,
) => {
if (!profile) {
return;
}
const original = frames;
for (let i = 0; i < position.length - 1; i++) {
const pos = position[i];
if (!frames[pos]) {
// TODO(mgechev): consider how to ensure we don't hit this case
console.warn('Unable to find parent node for', profile, original);
return;
}
frames = frames[pos].children;
}
const lastIdx = position[position.length - 1];
let lastFrame: ElementProfile = {
children: [],
directives: [],
};
if (frames[lastIdx]) {
lastFrame = frames[lastIdx];
} else {
frames[lastIdx] = lastFrame;
}
insertOrMerge(lastFrame, profile);
};
const prepareInitialFrame = (source: string, duration: number) => {
const frame: ProfilerFrame = {
@ -252,7 +285,7 @@ const prepareInitialFrame = (source: string, duration: number) => {
const directiveForestHooks = initializeOrGetDirectiveForestHooks();
const directiveForest = directiveForestHooks.getIndexedDirectiveForest();
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
let position: ElementPosition|undefined;
let position: ElementPosition | undefined;
if (node.component) {
position = directiveForestHooks.getDirectivePosition(node.component.instance);
} else {

View file

@ -28,7 +28,7 @@ export class DirectiveForestHooks {
profiler: Profiler = selectProfilerStrategy();
getDirectivePosition(dir: any): ElementPosition|undefined {
getDirectivePosition(dir: any): ElementPosition | undefined {
const result = this._tracker.getDirectivePosition(dir);
if (result === undefined) {
console.warn('Unable to find position of', dir);
@ -36,7 +36,7 @@ export class DirectiveForestHooks {
return result;
}
getDirectiveId(dir: any): number|undefined {
getDirectiveId(dir: any): number | undefined {
const result = this._tracker.getDirectiveId(dir);
if (result === undefined) {
console.warn('Unable to find ID of', result);

View file

@ -12,7 +12,7 @@ import {buildDirectiveForest} from '../component-tree';
import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces';
export declare interface Type<T> extends Function {
new(...args: any[]): T;
new (...args: any[]): T;
}
interface TreeNode {
@ -22,7 +22,8 @@ interface TreeNode {
}
export type NodeArray = {
directive: any; isComponent: boolean;
directive: any;
isComponent: boolean;
}[];
export class IdentityTracker {
@ -43,11 +44,11 @@ export class IdentityTracker {
return IdentityTracker._instance;
}
getDirectivePosition(dir: any): ElementPosition|undefined {
getDirectivePosition(dir: any): ElementPosition | undefined {
return this._currentDirectivePosition.get(dir);
}
getDirectiveId(dir: any): number|undefined {
getDirectiveId(dir: any): number | undefined {
return this._currentDirectiveId.get(dir);
}
@ -56,7 +57,9 @@ export class IdentityTracker {
}
index(): {
newNodes: NodeArray; removedNodes: NodeArray; indexedForest: IndexedNode[];
newNodes: NodeArray;
removedNodes: NodeArray;
indexedForest: IndexedNode[];
directiveForest: ComponentTreeNode[];
} {
const directiveForest = buildDirectiveForest();
@ -78,8 +81,11 @@ export class IdentityTracker {
}
private _index(
node: IndexedNode, parent: TreeNode|null, newNodes: {directive: any; isComponent: boolean}[],
allNodes: Set<any>): void {
node: IndexedNode,
parent: TreeNode | null,
newNodes: {directive: any; isComponent: boolean}[],
allNodes: Set<any>,
): void {
if (node.component) {
allNodes.add(node.component.instance);
this.isComponent.set(node.component.instance, true);
@ -113,7 +119,10 @@ export interface IndexedNode extends DevToolsNode<DirectiveInstanceType, Compone
}
const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
node: T, idx: number, parentPosition: number[] = []): IndexedNode => {
node: T,
idx: number,
parentPosition: number[] = [],
): IndexedNode => {
const position = parentPosition.concat([idx]);
return {
position,
@ -126,4 +135,5 @@ const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstan
};
export const indexForest = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
forest: T[]): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
forest: T[],
): IndexedNode[] => forest.map((n, i) => indexTree(n, i));

View file

@ -15,9 +15,9 @@ import {DirectiveForestHooks} from './hooks';
const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`;
const supportsPerformance =
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
type Method = keyof LifecycleProfile|'changeDetection'|string;
type Method = keyof LifecycleProfile | 'changeDetection' | string;
const recordMark = (s: string, method: Method) => {
if (supportsPerformance) {

View file

@ -15,32 +15,37 @@ import {IdentityTracker, NodeArray} from '../identity-tracker';
import {getLifeCycleName, Hooks, Profiler} from './shared';
type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
void;
type ProfilerCallback = (
event: ɵProfilerEvent,
instanceOrLView: {} | null,
hookOrListener: any,
) => void;
/** Implementation of Profiler that utilizes framework APIs fire profiler hooks. */
export class NgProfiler extends Profiler {
private _tracker = IdentityTracker.getInstance();
private _callbacks: ProfilerCallback[] = [];
private _lastDirectiveInstance: {}|null = null;
private _lastDirectiveInstance: {} | null = null;
constructor(config: Partial<Hooks> = {}) {
super(config);
this._setProfilerCallback(
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => {
if (this[event] === undefined) {
return;
}
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) => {
if (this[event] === undefined) {
return;
}
this[event](instanceOrLView, hookOrListener);
});
this[event](instanceOrLView, hookOrListener);
},
);
this._initialize();
}
private _initialize(): void {
ngDebugClient().ɵsetProfiler(
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)));
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) =>
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)),
);
}
private _setProfilerCallback(callback: ProfilerCallback): void {
@ -106,9 +111,11 @@ export class NgProfiler extends Profiler {
}
this._onChangeDetectionStart(
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
this._tracker.getDirectiveId(this._lastDirectiveInstance),
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
this._lastDirectiveInstance,
getDirectiveHostElement(this._lastDirectiveInstance),
this._tracker.getDirectiveId(this._lastDirectiveInstance),
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
);
}
[ɵProfilerEvent.TemplateUpdateEnd](context: any, _hookOrListener: any): void {
@ -121,9 +128,11 @@ export class NgProfiler extends Profiler {
}
this._onChangeDetectionEnd(
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
this._tracker.getDirectiveId(this._lastDirectiveInstance),
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
this._lastDirectiveInstance,
getDirectiveHostElement(this._lastDirectiveInstance),
this._tracker.getDirectiveId(this._lastDirectiveInstance),
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
);
}
[ɵProfilerEvent.LifecycleHookStart](directive: any, hook: any): void {

View file

@ -6,7 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME,} from '../../directive-forest';
import {
getDirectiveHostElement,
getLViewFromDirectiveOrElementInstance,
METADATA_PROPERTY_NAME,
} from '../../directive-forest';
import {runOutsideAngular} from '../../utils';
import {IdentityTracker, NodeArray} from '../identity-tracker';
@ -82,7 +86,7 @@ export class PatchingProfiler extends Profiler {
if (original.patched) {
return;
}
declarations.tView.template = function(_: any, component: any): void {
declarations.tView.template = function (_: any, component: any): void {
if (!self._inChangeDetection) {
self._inChangeDetection = true;
runOutsideAngular(() => {
@ -122,7 +126,7 @@ export class PatchingProfiler extends Profiler {
}
if (typeof el === 'function') {
const self = this;
current[idx] = function(): any {
current[idx] = function (): any {
// We currently don't want to notify the consumer
// for execution of lifecycle hooks of services and pipes.
// These two abstractions don't have `__ngContext__`, and

View file

@ -11,32 +11,64 @@ import {Subject} from 'rxjs';
import {NodeArray} from '../identity-tracker';
type CreationHook =
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
position: ElementPosition) => void;
type CreationHook = (
componentOrDirective: any,
node: Node,
id: number,
isComponent: boolean,
position: ElementPosition,
) => void;
type LifecycleStartHook =
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
isComponent: boolean) => void;
type LifecycleStartHook = (
componentOrDirective: any,
hook: keyof LifecycleProfile,
node: Node,
id: number,
isComponent: boolean,
) => void;
type LifecycleEndHook =
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
isComponent: boolean) => void;
type LifecycleEndHook = (
componentOrDirective: any,
hook: keyof LifecycleProfile,
node: Node,
id: number,
isComponent: boolean,
) => void;
type ChangeDetectionStartHook =
(component: any, node: Node, id: number, position: ElementPosition) => void;
type ChangeDetectionStartHook = (
component: any,
node: Node,
id: number,
position: ElementPosition,
) => void;
type ChangeDetectionEndHook = (component: any, node: Node, id: number, position: ElementPosition) =>
void;
type ChangeDetectionEndHook = (
component: any,
node: Node,
id: number,
position: ElementPosition,
) => void;
type DestroyHook =
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
position: ElementPosition) => void;
type DestroyHook = (
componentOrDirective: any,
node: Node,
id: number,
isComponent: boolean,
position: ElementPosition,
) => void;
type OutputStartHook =
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
type OutputEndHook =
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
type OutputStartHook = (
componentOrDirective: any,
outputName: string,
node: Node,
isComponent: boolean,
) => void;
type OutputEndHook = (
componentOrDirective: any,
outputName: string,
node: Node,
isComponent: boolean,
) => void;
export interface Hooks {
onCreate: CreationHook;
@ -79,8 +111,12 @@ export abstract class Profiler {
/** @internal */
protected _onCreate(
_: any, __: Node, id: number|undefined, ___: boolean,
position: ElementPosition|undefined): void {
_: any,
__: Node,
id: number | undefined,
___: boolean,
position: ElementPosition | undefined,
): void {
if (id === undefined || position === undefined) {
return;
}
@ -89,8 +125,12 @@ export abstract class Profiler {
/** @internal */
protected _onDestroy(
_: any, __: Node, id: number|undefined, ___: boolean,
position: ElementPosition|undefined): void {
_: any,
__: Node,
id: number | undefined,
___: boolean,
position: ElementPosition | undefined,
): void {
if (id === undefined || position === undefined) {
return;
}
@ -99,7 +139,11 @@ export abstract class Profiler {
/** @internal */
protected _onChangeDetectionStart(
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
_: any,
__: Node,
id: number | undefined,
position: ElementPosition | undefined,
): void {
if (id === undefined || position === undefined) {
return;
}
@ -108,7 +152,11 @@ export abstract class Profiler {
/** @internal */
protected _onChangeDetectionEnd(
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
_: any,
__: Node,
id: number | undefined,
position: ElementPosition | undefined,
): void {
if (id === undefined || position === undefined) {
return;
}
@ -117,8 +165,12 @@ export abstract class Profiler {
/** @internal */
protected _onLifecycleHookStart(
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
____: boolean): void {
_: any,
__: keyof LifecycleProfile | 'unknown',
___: Node,
id: number | undefined,
____: boolean,
): void {
if (id === undefined) {
return;
}
@ -127,8 +179,12 @@ export abstract class Profiler {
/** @internal */
protected _onLifecycleHookEnd(
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
____: boolean): void {
_: any,
__: keyof LifecycleProfile | 'unknown',
___: Node,
id: number | undefined,
____: boolean,
): void {
if (id === undefined) {
return;
}
@ -136,8 +192,13 @@ export abstract class Profiler {
}
/** @internal */
protected _onOutputStart(_: any, __: string, ___: Node, id: number|undefined, ____: boolean):
void {
protected _onOutputStart(
_: any,
__: string,
___: Node,
id: number | undefined,
____: boolean,
): void {
if (id === undefined) {
return;
}
@ -145,7 +206,13 @@ export abstract class Profiler {
}
/** @internal */
protected _onOutputEnd(_: any, __: string, ___: Node, id: number|undefined, ____: boolean): void {
protected _onOutputEnd(
_: any,
__: string,
___: Node,
id: number | undefined,
____: boolean,
): void {
if (id === undefined) {
return;
}
@ -176,7 +243,7 @@ const hookNames = [
const hookMethodNames = new Set(hookNames.map((hook) => `ng${hook}`));
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile|'unknown' => {
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile | 'unknown' => {
const proto = Object.getPrototypeOf(obj);
const keys = Object.getOwnPropertyNames(proto);
for (const propName of keys) {

View file

@ -24,7 +24,7 @@ export interface ComponentInstanceType {
isElement: boolean;
}
export interface ComponentTreeNode extends
DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
export interface ComponentTreeNode
extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
children: ComponentTreeNode[];
}

View file

@ -31,7 +31,7 @@ export function parseRoutes(router: Router): Route {
return root;
}
function assignChildrenToParent(parentPath: string|null, children: Routes): Route[] {
function assignChildrenToParent(parentPath: string | null, children: Routes): Route[] {
return children.map((child: AngularRoute) => {
const childName = childRouteName(child);
const childDescendents: [any] = (child as any)._loadedConfig?.routes || child.children;

View file

@ -12,7 +12,7 @@ import {arrayEquals} from 'shared-utils';
import {ComponentTreeNode} from './interfaces';
interface ConsoleReferenceNode {
node: ComponentTreeNode|null;
node: ComponentTreeNode | null;
position: ElementPosition;
}
@ -35,8 +35,9 @@ const _setConsoleReference = (referenceNode: ConsoleReferenceNode) => {
};
const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNode) => {
const foundIndex = nodesForConsoleReference.findIndex(
(nodeToLookFor) => arrayEquals(nodeToLookFor.position, referenceNode.position));
const foundIndex = nodesForConsoleReference.findIndex((nodeToLookFor) =>
arrayEquals(nodeToLookFor.position, referenceNode.position),
);
if (foundIndex !== -1) {
nodesForConsoleReference.splice(foundIndex, 1);
} else if (nodesForConsoleReference.length === CAPACITY) {
@ -45,12 +46,12 @@ const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNod
};
const assignConsoleReferencesFrom = (referenceNodes: ConsoleReferenceNode[]) => {
referenceNodes.forEach(
(referenceNode, index) =>
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)));
referenceNodes.forEach((referenceNode, index) =>
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)),
);
};
const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
const setDirectiveKey = (node: ComponentTreeNode | null, key: string) => {
Object.defineProperty(window, key, {
get: () => {
if (node?.component) {
@ -66,4 +67,4 @@ const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
};
const getConsoleReferenceWithIndexOf = (consoleReferenceIndex: number) =>
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;

View file

@ -24,7 +24,7 @@ export function getKeys(obj: {}): string[] {
const prototypeMembers = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(obj));
const ignoreList = ['__proto__'];
const gettersAndSetters = Object.keys(prototypeMembers).filter(methodName => {
const gettersAndSetters = Object.keys(prototypeMembers).filter((methodName) => {
if (ignoreList.includes(methodName)) {
return false;
}
@ -42,6 +42,6 @@ export function getKeys(obj: {}): string[] {
* @param propName The string representation of the target property name
* @returns The Descriptor object of the property
*/
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor|undefined =>
Object.getOwnPropertyDescriptor(instance, propName) ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor | undefined =>
Object.getOwnPropertyDescriptor(instance, propName) ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);

View file

@ -73,11 +73,14 @@ describe('getPropType', () => {
propTypeName: 'Set',
},
{
expression:
new Map<unknown, unknown>([['name', 'John'], ['age', 40], [{id: 123}, undefined]]),
expression: new Map<unknown, unknown>([
['name', 'John'],
['age', 40],
[{id: 123}, undefined],
]),
propType: PropType.Map,
propTypeName: 'Map',
}
},
];
for (const {expression, propType, propTypeName} of testCases) {
it(`should determine ${String(expression)} as PropType:${propTypeName}(${propType})`, () => {

View file

@ -15,7 +15,7 @@ import {getDescriptor, getKeys} from './object-utils';
// todo(aleksanderbodurri) pull this out of this file
const METADATA_PROPERTY_NAME = '__ngContext__';
type NestedType = PropType.Array|PropType.Object;
type NestedType = PropType.Array | PropType.Object;
export interface CompositeType {
type: Extract<PropType, NestedType>;
@ -29,7 +29,7 @@ export interface TerminalType {
containerType: ContainerType;
}
export type PropertyData = TerminalType|CompositeType;
export type PropertyData = TerminalType | CompositeType;
export type Formatter<Result> = {
[key in PropType]: (data: any) => Result;
@ -73,107 +73,121 @@ const typeToDescriptorPreview: Formatter<string> = {
[PropType.Unknown]: (_: any) => 'unknown',
};
type Key = string|number;
type NestedSerializerFn =
(instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
currentLevel: number, level?: number) => Descriptor;
type Key = string | number;
type NestedSerializerFn = (
instance: any,
propName: string | number,
nodes: NestedProp[],
isReadonly: boolean,
currentLevel: number,
level?: number,
) => Descriptor;
const ignoreList: Set<Key> = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
const shallowPropTypeToTreeMetaData:
Record<Exclude<PropType, NestedType>, {editable: boolean, expandable: boolean}> = {
[PropType.String]: {
editable: true,
expandable: false,
},
[PropType.BigInt]: {
editable: false,
expandable: false,
},
[PropType.Boolean]: {
editable: true,
expandable: false,
},
[PropType.Number]: {
editable: true,
expandable: false,
},
[PropType.Date]: {
editable: false,
expandable: false,
},
[PropType.Null]: {
editable: true,
expandable: false,
},
[PropType.Undefined]: {
editable: true,
expandable: false,
},
[PropType.Symbol]: {
editable: false,
expandable: false,
},
[PropType.Function]: {
editable: false,
expandable: false,
},
[PropType.HTMLNode]: {
editable: false,
expandable: false,
},
[PropType.Unknown]: {
editable: false,
expandable: false,
},
[PropType.Set]: {
editable: false,
expandable: false,
},
[PropType.Map]: {
editable: false,
expandable: false,
},
};
const shallowPropTypeToTreeMetaData: Record<
Exclude<PropType, NestedType>,
{editable: boolean; expandable: boolean}
> = {
[PropType.String]: {
editable: true,
expandable: false,
},
[PropType.BigInt]: {
editable: false,
expandable: false,
},
[PropType.Boolean]: {
editable: true,
expandable: false,
},
[PropType.Number]: {
editable: true,
expandable: false,
},
[PropType.Date]: {
editable: false,
expandable: false,
},
[PropType.Null]: {
editable: true,
expandable: false,
},
[PropType.Undefined]: {
editable: true,
expandable: false,
},
[PropType.Symbol]: {
editable: false,
expandable: false,
},
[PropType.Function]: {
editable: false,
expandable: false,
},
[PropType.HTMLNode]: {
editable: false,
expandable: false,
},
[PropType.Unknown]: {
editable: false,
expandable: false,
},
[PropType.Set]: {
editable: false,
expandable: false,
},
[PropType.Map]: {
editable: false,
expandable: false,
},
};
const isEditable =
(descriptor: PropertyDescriptor|undefined, propName: string|number, propData: TerminalType,
isGetterOrSetter: boolean) => {
if (propData.containerType === 'ReadonlySignal') {
return false;
}
const isEditable = (
descriptor: PropertyDescriptor | undefined,
propName: string | number,
propData: TerminalType,
isGetterOrSetter: boolean,
) => {
if (propData.containerType === 'ReadonlySignal') {
return false;
}
if (typeof propName === 'symbol') {
return false;
}
if (typeof propName === 'symbol') {
return false;
}
if (isGetterOrSetter) {
return false;
}
if (isGetterOrSetter) {
return false;
}
if (descriptor?.writable === false) {
return false;
}
if (descriptor?.writable === false) {
return false;
}
return shallowPropTypeToTreeMetaData[propData.type].editable;
};
return shallowPropTypeToTreeMetaData[propData.type].editable;
};
const isGetterOrSetter = (descriptor: any): boolean =>
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
const getPreview = (propData: TerminalType|CompositeType, isGetterOrSetter: boolean) => {
const getPreview = (propData: TerminalType | CompositeType, isGetterOrSetter: boolean) => {
if (propData.containerType === 'ReadonlySignal') {
return `Readonly Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
} else if (propData.containerType === 'WritableSignal') {
return `Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
}
return !isGetterOrSetter ? typeToDescriptorPreview[propData.type](propData.prop) :
typeToDescriptorPreview[PropType.Function]({name: ''});
return !isGetterOrSetter
? typeToDescriptorPreview[propData.type](propData.prop)
: typeToDescriptorPreview[PropType.Function]({name: ''});
};
export function createShallowSerializedDescriptor(
instance: any, propName: string|number, propData: TerminalType,
isReadonly: boolean): Descriptor {
instance: any,
propName: string | number,
propData: TerminalType,
isReadonly: boolean,
): Descriptor {
const {type, containerType} = propData;
const descriptor = getDescriptor(instance, propName as string);
@ -195,10 +209,18 @@ export function createShallowSerializedDescriptor(
}
export function createLevelSerializedDescriptor(
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
continuation: (
instance: any, propName: string|number, isReadonly: boolean, level?: number,
max?: number) => Descriptor): Descriptor {
instance: {},
propName: string | number,
propData: CompositeType,
levelOptions: LevelOptions,
continuation: (
instance: any,
propName: string | number,
isReadonly: boolean,
level?: number,
max?: number,
) => Descriptor,
): Descriptor {
const {type, prop, containerType} = propData;
const descriptor = getDescriptor(instance, propName as string);
@ -223,8 +245,13 @@ export function createLevelSerializedDescriptor(
}
export function createNestedSerializedDescriptor(
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
nodes: NestedProp[], nestedSerializer: NestedSerializerFn): Descriptor {
instance: {},
propName: string | number,
propData: CompositeType,
levelOptions: LevelOptions,
nodes: NestedProp[],
nestedSerializer: NestedSerializerFn,
): Descriptor {
const {type, prop, containerType} = propData;
const descriptor = getDescriptor(instance, propName as string);
@ -248,49 +275,75 @@ export function createNestedSerializedDescriptor(
}
function getNestedDescriptorValue(
propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[],
nestedSerializer: NestedSerializerFn) {
propData: CompositeType,
levelOptions: LevelOptions,
nodes: NestedProp[],
nestedSerializer: NestedSerializerFn,
) {
const {type, prop} = propData;
const {currentLevel} = levelOptions;
const value = unwrapSignal(prop);
switch (type) {
case PropType.Array:
return nodes.map(
(nestedProp) => nestedSerializer(
value, nestedProp.name, nestedProp.children, false, currentLevel + 1));
return nodes.map((nestedProp) =>
nestedSerializer(value, nestedProp.name, nestedProp.children, false, currentLevel + 1),
);
case PropType.Object:
return nodes.reduce((accumulator, nestedProp) => {
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
accumulator[nestedProp.name] = nestedSerializer(
value, nestedProp.name, nestedProp.children, false, currentLevel + 1);
}
return accumulator;
}, {} as Record<string, Descriptor>);
return nodes.reduce(
(accumulator, nestedProp) => {
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
accumulator[nestedProp.name] = nestedSerializer(
value,
nestedProp.name,
nestedProp.children,
false,
currentLevel + 1,
);
}
return accumulator;
},
{} as Record<string, Descriptor>,
);
}
}
function getLevelDescriptorValue(
propData: CompositeType, levelOptions: LevelOptions,
continuation: (
instance: any, propName: string|number, isReadonly: boolean, level?: number,
max?: number) => Descriptor) {
propData: CompositeType,
levelOptions: LevelOptions,
continuation: (
instance: any,
propName: string | number,
isReadonly: boolean,
level?: number,
max?: number,
) => Descriptor,
) {
const {type, prop} = propData;
const {currentLevel, level} = levelOptions;
const value = unwrapSignal(prop);
const isReadonly = isSignal(prop);
switch (type) {
case PropType.Array:
return prop.map(
(_: any, idx: number) => continuation(value, idx, isReadonly, currentLevel + 1, level));
return prop.map((_: any, idx: number) =>
continuation(value, idx, isReadonly, currentLevel + 1, level),
);
case PropType.Object:
return getKeys(prop).reduce((accumulator, propName) => {
if (!ignoreList.has(propName)) {
accumulator[propName] =
continuation(value, propName, isReadonly, currentLevel + 1, level);
}
return accumulator;
}, {} as Record<string, Descriptor>);
return getKeys(prop).reduce(
(accumulator, propName) => {
if (!ignoreList.has(propName)) {
accumulator[propName] = continuation(
value,
propName,
isReadonly,
currentLevel + 1,
level,
);
}
return accumulator;
},
{} as Record<string, Descriptor>,
);
}
}

View file

@ -93,7 +93,7 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '1',
value: 1,
containerType: null
containerType: null,
},
nested: {
type: PropType.Object,
@ -106,10 +106,10 @@ describe('deeplySerializeSelectedProperties', () => {
expandable: true,
editable: false,
preview: 'Array(4)',
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
@ -123,7 +123,7 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '1',
value: 1,
containerType: null
containerType: null,
},
nested: {
type: PropType.Object,
@ -155,26 +155,26 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '1',
value: 1,
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
],
containerType: null
containerType: null,
},
{
type: PropType.Set,
editable: false,
expandable: false,
preview: 'Set(2)',
containerType: null
containerType: null,
},
],
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
@ -202,36 +202,37 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
expandable: true,
preview: 'Array(3)',
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
it('should work with getters with specified query', () => {
const result = deeplySerializeSelectedProperties(
{
get foo(): any {
return {
baz: {
qux: 3,
},
};
},
{
get foo(): any {
return {
baz: {
qux: 3,
},
};
},
[
{
name: 'foo',
children: [
{
name: 'baz',
children: [],
},
],
},
]);
},
[
{
name: 'foo',
children: [
{
name: 'baz',
children: [],
},
],
},
],
);
expect(result).toEqual({
foo: {
type: PropType.Object,
@ -244,28 +245,29 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
expandable: true,
preview: '{...}',
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
it('should work with getters without specified query', () => {
const result = deeplySerializeSelectedProperties(
{
get foo(): any {
return {
baz: {
qux: {
cos: 3,
},
{
get foo(): any {
return {
baz: {
qux: {
cos: 3,
},
};
},
},
};
},
[]);
},
[],
);
expect(result).toEqual({
foo: {
type: PropType.Object,
@ -278,26 +280,27 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
expandable: true,
preview: '{...}',
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
it('both getters and setters should be readonly', () => {
const result = deeplySerializeSelectedProperties(
{
get foo(): number {
return 42;
},
get bar(): number {
return 42;
},
set bar(val: number) {},
{
get foo(): number {
return 42;
},
[]);
get bar(): number {
return 42;
},
set bar(val: number) {},
},
[],
);
// Neither getter and setter is editable
expect(result).toEqual({
@ -307,7 +310,7 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
preview: '(...)',
value: 42,
containerType: null
containerType: null,
},
bar: {
type: PropType.Number,
@ -315,58 +318,59 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
preview: '(...)',
value: 42,
containerType: null
containerType: null,
},
});
});
it('should return the precise path requested', () => {
const result = deeplySerializeSelectedProperties(
{
state: {
nested: {
props: {
foo: 1,
bar: 2,
},
[Symbol(3)](): number {
return 1.618;
},
get foo(): number {
return 42;
},
{
state: {
nested: {
props: {
foo: 1,
bar: 2,
},
[Symbol(3)](): number {
return 1.618;
},
get foo(): number {
return 42;
},
},
},
[
{
name: 'state',
children: [
{
name: 'nested',
children: [
{
name: 'props',
children: [
{
name: 'foo',
children: [],
},
{
name: 'bar',
children: [],
},
],
},
{
name: 'foo',
children: [],
},
],
},
],
},
]);
},
[
{
name: 'state',
children: [
{
name: 'nested',
children: [
{
name: 'props',
children: [
{
name: 'foo',
children: [],
},
{
name: 'bar',
children: [],
},
],
},
{
name: 'foo',
children: [],
},
],
},
],
},
],
);
expect(result).toEqual({
state: {
type: PropType.Object,
@ -392,7 +396,7 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '1',
value: 1,
containerType: null
containerType: null,
},
bar: {
type: PropType.Number,
@ -400,10 +404,10 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '2',
value: 2,
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
foo: {
type: PropType.Number,
@ -411,33 +415,34 @@ describe('deeplySerializeSelectedProperties', () => {
editable: false,
preview: '(...)',
value: 42,
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
},
containerType: null
containerType: null,
},
});
});
it('both setter and getter would get a (...) as preview', () => {
const result = deeplySerializeSelectedProperties(
{
set foo(_: any) {},
get bar(): Object {
return {foo: 1};
},
{
set foo(_: any) {},
get bar(): Object {
return {foo: 1};
},
[]);
},
[],
);
expect(result).toEqual({
foo: {
type: PropType.Undefined,
editable: false,
expandable: false,
preview: '(...)',
containerType: null
containerType: null,
},
bar: {
type: PropType.Object,
@ -452,7 +457,7 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
preview: '1',
value: 1,
containerType: null
containerType: null,
},
},
},
@ -467,50 +472,49 @@ describe('deeplySerializeSelectedProperties', () => {
editable: true,
expandable: false,
preview: 'undefined',
containerType: null
}
containerType: null,
},
});
});
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype',
() => {
const instance = {
__proto__: {
get foo(): number {
return 42;
},
set bar(newNum: number) {},
get baz(): number {
return 42;
},
set baz(newNum: number) {},
}
};
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype', () => {
const instance = {
__proto__: {
get foo(): number {
return 42;
},
set bar(newNum: number) {},
get baz(): number {
return 42;
},
set baz(newNum: number) {},
},
};
const descriptorFoo = getDescriptor(instance, 'foo');
expect(descriptorFoo).not.toBeNull();
expect(descriptorFoo!.get).not.toBeNull();
expect(descriptorFoo!.set).toBeUndefined();
expect(descriptorFoo!.value).toBeUndefined();
expect(descriptorFoo!.enumerable).toBe(true);
expect(descriptorFoo!.configurable).toBe(true);
const descriptorFoo = getDescriptor(instance, 'foo');
expect(descriptorFoo).not.toBeNull();
expect(descriptorFoo!.get).not.toBeNull();
expect(descriptorFoo!.set).toBeUndefined();
expect(descriptorFoo!.value).toBeUndefined();
expect(descriptorFoo!.enumerable).toBe(true);
expect(descriptorFoo!.configurable).toBe(true);
const descriptorBar = getDescriptor(instance, 'bar');
expect(descriptorBar).not.toBeNull();
expect(descriptorBar!.get).toBeUndefined();
expect(descriptorBar!.set).not.toBeNull();
expect(descriptorBar!.value).toBeUndefined();
expect(descriptorBar!.enumerable).toBe(true);
expect(descriptorBar!.configurable).toBe(true);
const descriptorBar = getDescriptor(instance, 'bar');
expect(descriptorBar).not.toBeNull();
expect(descriptorBar!.get).toBeUndefined();
expect(descriptorBar!.set).not.toBeNull();
expect(descriptorBar!.value).toBeUndefined();
expect(descriptorBar!.enumerable).toBe(true);
expect(descriptorBar!.configurable).toBe(true);
const descriptorBaz = getDescriptor(instance, 'baz');
expect(descriptorBaz).not.toBeNull();
expect(descriptorBaz!.get).not.toBeNull();
expect(descriptorBaz!.set).not.toBeNull();
expect(descriptorBaz!.value).toBeUndefined();
expect(descriptorBaz!.enumerable).toBe(true);
expect(descriptorBaz!.configurable).toBe(true);
});
const descriptorBaz = getDescriptor(instance, 'baz');
expect(descriptorBaz).not.toBeNull();
expect(descriptorBaz!.get).not.toBeNull();
expect(descriptorBaz!.set).not.toBeNull();
expect(descriptorBaz!.value).toBeUndefined();
expect(descriptorBaz!.enumerable).toBe(true);
expect(descriptorBaz!.configurable).toBe(true);
});
it('getKeys should all keys including getters and setters', () => {
const instance = {
@ -520,8 +524,8 @@ describe('deeplySerializeSelectedProperties', () => {
return 42;
},
set foo(newNum: number) {},
set bar(newNum: number) {}
}
set bar(newNum: number) {},
},
};
expect(getKeys(instance)).toEqual(['baz', 'foo', 'bar']);
@ -534,8 +538,8 @@ describe('deeplySerializeSelectedProperties', () => {
set __proto__(newObj: Object) {},
get __proto__(): Object {
return {};
}
}
},
},
};
expect(getKeys(instance)).toEqual(['baz']);

View file

@ -13,7 +13,12 @@ import {isSignal, unwrapSignal} from '../utils';
import {getKeys} from './object-utils';
import {getPropType} from './prop-type';
import {createLevelSerializedDescriptor, createNestedSerializedDescriptor, createShallowSerializedDescriptor, PropertyData,} from './serialized-descriptor-factory';
import {
createLevelSerializedDescriptor,
createNestedSerializedDescriptor,
createShallowSerializedDescriptor,
PropertyData,
} from './serialized-descriptor-factory';
// todo(aleksanderbodurri) pull this out of this file
const METADATA_PROPERTY_NAME = '__ngContext__';
@ -23,23 +28,32 @@ const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
const MAX_LEVEL = 1;
function nestedSerializer(
instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
currentLevel = 0, level = MAX_LEVEL): Descriptor {
instance: any,
propName: string | number,
nodes: NestedProp[],
isReadonly: boolean,
currentLevel = 0,
level = MAX_LEVEL,
): Descriptor {
instance = unwrapSignal(instance);
const serializableInstance = instance[propName];
const propData: PropertyData = {
prop: serializableInstance,
type: getPropType(serializableInstance),
containerType: getContainerType(serializableInstance)
containerType: getContainerType(serializableInstance),
};
if (currentLevel < level) {
const continuation =
(instance: any, propName: string|number, isReadonly: boolean, nestedLevel?: number,
_?: number) => {
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
};
const continuation = (
instance: any,
propName: string | number,
isReadonly: boolean,
nestedLevel?: number,
_?: number,
) => {
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
};
return levelSerializer(instance, propName, isReadonly, currentLevel, level, continuation);
}
@ -48,27 +62,43 @@ function nestedSerializer(
case PropType.Array:
case PropType.Object:
return createNestedSerializedDescriptor(
instance, propName, propData, {level, currentLevel}, nodes, nestedSerializer);
instance,
propName,
propData,
{level, currentLevel},
nodes,
nestedSerializer,
);
default:
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
}
}
function levelSerializer(
instance: any, propName: string|number, isReadonly: boolean, currentLevel = 0,
level = MAX_LEVEL, continuation = levelSerializer): Descriptor {
instance: any,
propName: string | number,
isReadonly: boolean,
currentLevel = 0,
level = MAX_LEVEL,
continuation = levelSerializer,
): Descriptor {
const serializableInstance = instance[propName];
const propData: PropertyData = {
prop: serializableInstance,
type: getPropType(serializableInstance),
containerType: getContainerType(serializableInstance)
containerType: getContainerType(serializableInstance),
};
switch (propData.type) {
case PropType.Array:
case PropType.Object:
return createLevelSerializedDescriptor(
instance, propName, propData, {level, currentLevel}, continuation);
instance,
propName,
propData,
{level, currentLevel},
continuation,
);
default:
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
}
@ -88,7 +118,9 @@ export function serializeDirectiveState(instance: object): Record<string, Descri
}
export function deeplySerializeSelectedProperties(
instance: object, props: NestedProp[]): Record<string, Descriptor> {
instance: object,
props: NestedProp[],
): Record<string, Descriptor> {
const result: Record<string, Descriptor> = {};
const isReadonly = isSignal(instance);
getKeys(instance).forEach((prop) => {
@ -105,7 +137,6 @@ export function deeplySerializeSelectedProperties(
return result;
}
function getContainerType(instance: unknown): ContainerType {
if (isSignal(instance)) {
return isWritableSignal(instance) ? 'WritableSignal' : 'ReadonlySignal';

View file

@ -59,8 +59,7 @@ export function ngDebugApiIsSupported(api: string): boolean {
return typeof ng[api] === 'function';
}
export function isSignal(prop: unknown): prop is() => unknown {
export function isSignal(prop: unknown): prop is () => unknown {
if (!ngDebugApiIsSupported('isSignal')) {
return false;
}

View file

@ -27,18 +27,17 @@ export type InjectorTreeD3Node = d3.HierarchyPointNode<InjectorTreeNode>;
export abstract class GraphRenderer<T, U> {
abstract render(graph: T): void;
abstract getNodeById(id: string): U|null;
abstract getNodeById(id: string): U | null;
abstract snapToNode(node: U): void;
abstract snapToRoot(): void;
abstract zoomScale(scale: number): void;
abstract root: U|null;
abstract root: U | null;
abstract get graphElement(): HTMLElement;
protected nodeClickListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
protected nodeMouseoverListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
protected nodeMouseoutListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
cleanup(): void {
this.nodeClickListeners = [];
this.nodeMouseoverListeners = [];
@ -59,7 +58,7 @@ export abstract class GraphRenderer<T, U> {
}
interface InjectorTreeVisualizerConfig {
orientation: 'horizontal'|'vertical';
orientation: 'horizontal' | 'vertical';
nodeSize: [width: number, height: number];
nodeSeparation: (nodeA: InjectorTreeD3Node, nodeB: InjectorTreeD3Node) => number;
nodeLabelSize: [width: number, height: number];
@ -69,14 +68,14 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
public config: InjectorTreeVisualizerConfig;
constructor(
private _containerElement: HTMLElement,
private _graphElement: HTMLElement,
{
orientation = 'horizontal',
nodeSize = [200, 500],
nodeSeparation = () => 2,
nodeLabelSize = [250, 60],
}: Partial<InjectorTreeVisualizerConfig> = {},
private _containerElement: HTMLElement,
private _graphElement: HTMLElement,
{
orientation = 'horizontal',
nodeSize = [200, 500],
nodeSeparation = () => 2,
nodeLabelSize = [250, 60],
}: Partial<InjectorTreeVisualizerConfig> = {},
) {
super();
@ -90,13 +89,15 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
private d3 = d3;
override root: InjectorTreeD3Node|null = null;
zoomController: d3.ZoomBehavior<HTMLElement, unknown>|null = null;
override root: InjectorTreeD3Node | null = null;
zoomController: d3.ZoomBehavior<HTMLElement, unknown> | null = null;
override zoomScale(scale: number) {
if (this.zoomController) {
this.zoomController.scaleTo(
this.d3.select<HTMLElement, unknown>(this._containerElement), scale);
this.d3.select<HTMLElement, unknown>(this._containerElement),
scale,
);
}
}
@ -108,8 +109,8 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
override snapToNode(node: InjectorTreeD3Node, scale = 1): void {
const svg = this.d3.select(this._containerElement);
const halfWidth = (this._containerElement.clientWidth / 2);
const halfHeight = (this._containerElement.clientHeight / 2);
const halfWidth = this._containerElement.clientWidth / 2;
const halfHeight = this._containerElement.clientHeight / 2;
const t = d3.zoomIdentity.translate(halfWidth - node.y, halfHeight - node.x).scale(scale);
svg.transition().duration(500).call(this.zoomController!.transform, t);
}
@ -118,9 +119,10 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
return this._graphElement;
}
override getNodeById(id: string): InjectorTreeD3Node|null {
const selection = this.d3.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
.select(`.node[data-id="${id}"]`);
override getNodeById(id: string): InjectorTreeD3Node | null {
const selection = this.d3
.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
.select(`.node[data-id="${id}"]`);
if (selection.empty()) {
return null;
}
@ -157,142 +159,128 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
this.root = nodes;
arrowDefId++;
svg.append('svg:defs')
.selectAll('marker')
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', 0)
.attr('class', 'arrow')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
svg
.append('svg:defs')
.selectAll('marker')
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', 0)
.attr('class', 'arrow')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
g.selectAll('.link')
.data(nodes.descendants().slice(1))
.enter()
.append('path')
.attr(
'class',
(node: InjectorTreeD3Node) => {
const parentId = node.parent?.data?.injector?.id;
if (parentId === 'N/A') {
return 'link-hidden';
}
.data(nodes.descendants().slice(1))
.enter()
.append('path')
.attr('class', (node: InjectorTreeD3Node) => {
const parentId = node.parent?.data?.injector?.id;
if (parentId === 'N/A') {
return 'link-hidden';
}
return `link`;
})
.attr(
'data-id',
(node: InjectorTreeD3Node) => {
const from = node.data.injector.id;
const to = node.parent?.data?.injector?.id;
return `link`;
})
.attr('data-id', (node: InjectorTreeD3Node) => {
const from = node.data.injector.id;
const to = node.parent?.data?.injector?.id;
if (from && to) {
return `${from}-to-${to}`;
}
return '';
})
.attr('marker-end', `url(#end${arrowDefId})`)
.attr('d', (node: InjectorTreeD3Node) => {
const parent = node.parent!;
if (this.config.orientation === 'horizontal') {
return `
if (from && to) {
return `${from}-to-${to}`;
}
return '';
})
.attr('marker-end', `url(#end${arrowDefId})`)
.attr('d', (node: InjectorTreeD3Node) => {
const parent = node.parent!;
if (this.config.orientation === 'horizontal') {
return `
M${node.y},${node.x}
C${(node.y + parent.y) / 2},
${node.x} ${(node.y + parent.y) / 2},
${parent.x} ${parent.y},
${parent.x}`;
}
}
return `
return `
M${node.x},${node.y}
C${(node.x + parent.x) / 2},
${node.y} ${(node.x + parent.x) / 2},
${parent.y} ${parent.x},
${parent.y}`;
});
});
// Declare the nodes
const node =
g.selectAll('g.node')
.data(nodes.descendants())
.enter()
.append('g')
.attr(
'class',
(node: InjectorTreeD3Node) => {
if (node.data.injector.id === 'N/A') {
return 'node-hidden';
}
return `node`;
})
.attr(
'data-component-id',
(node: InjectorTreeD3Node) => {
const injector = node.data.injector;
if (injector.type === 'element') {
return injector.node?.component?.id ?? -1;
}
return -1;
})
.attr(
'data-id',
(node: InjectorTreeD3Node) => {
return node.data.injector.id;
})
.on('click',
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeClickListeners.forEach(listener => listener(pointerEvent, node));
})
.on('mouseover',
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeMouseoverListeners.forEach(listener => listener(pointerEvent, node));
})
.on('mouseout',
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeMouseoutListeners.forEach(listener => listener(pointerEvent, node));
})
const node = g
.selectAll('g.node')
.data(nodes.descendants())
.enter()
.append('g')
.attr('class', (node: InjectorTreeD3Node) => {
if (node.data.injector.id === 'N/A') {
return 'node-hidden';
}
return `node`;
})
.attr('data-component-id', (node: InjectorTreeD3Node) => {
const injector = node.data.injector;
if (injector.type === 'element') {
return injector.node?.component?.id ?? -1;
}
return -1;
})
.attr('data-id', (node: InjectorTreeD3Node) => {
return node.data.injector.id;
})
.on('click', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeClickListeners.forEach((listener) => listener(pointerEvent, node));
})
.on('mouseover', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeMouseoverListeners.forEach((listener) => listener(pointerEvent, node));
})
.on('mouseout', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
this.nodeMouseoutListeners.forEach((listener) => listener(pointerEvent, node));
})
.attr('transform', (node: InjectorTreeD3Node) => {
if (this.config.orientation === 'horizontal') {
return `translate(${node.y},${node.x})`;
}
.attr('transform', (node: InjectorTreeD3Node) => {
if (this.config.orientation === 'horizontal') {
return `translate(${node.y},${node.x})`;
}
return `translate(${node.x},${node.y})`;
});
return `translate(${node.x},${node.y})`;
});
const [width, height] = this.config.nodeLabelSize!;
node.append('foreignObject')
.attr('width', width)
.attr('height', height)
.attr('x', -1 * (width - 10))
.attr('y', -1 * (height / 2))
.append('xhtml:div')
.attr(
'title',
(node: InjectorTreeD3Node) => {
return node.data.injector.name;
})
.attr(
'class',
(node: InjectorTreeD3Node) => {
return [
injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'
].join(' ');
})
.html((node: InjectorTreeD3Node) => {
const label = node.data.injector.name;
const lengthLimit = 25;
return label.length > lengthLimit ? label.slice(0, lengthLimit - '...'.length) + '...' :
label;
});
node
.append('foreignObject')
.attr('width', width)
.attr('height', height)
.attr('x', -1 * (width - 10))
.attr('y', -1 * (height / 2))
.append('xhtml:div')
.attr('title', (node: InjectorTreeD3Node) => {
return node.data.injector.name;
})
.attr('class', (node: InjectorTreeD3Node) => {
return [injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'].join(
' ',
);
})
.html((node: InjectorTreeD3Node) => {
const label = node.data.injector.name;
const lengthLimit = 25;
return label.length > lengthLimit
? label.slice(0, lengthLimit - '...'.length) + '...'
: label;
});
svg.attr('height', '100%').attr('width', '100%');
}

View file

@ -14,20 +14,26 @@ import {InjectorTreeNode, InjectorTreeVisualizer} from './injector-tree-visualiz
@Component({
selector: 'ng-resolution-path',
template: `
<section class="injector-graph">
<svg #svgContainer>
<g #mainGroup></g>
</svg>
</section>
<section class="injector-graph">
<svg #svgContainer>
<g #mainGroup></g>
</svg>
</section>
`,
styles: [
`
:host {
display: block;
}
`,
styles: [`:host { display: block; }`],
standalone: true
],
standalone: true,
})
export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
@ViewChild('svgContainer', {static: true}) private svgContainer!: ElementRef;
@ViewChild('mainGroup', {static: true}) private g!: ElementRef;
@Input() orientation: 'horizontal'|'vertical' = 'horizontal';
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
private injectorTree!: InjectorTreeVisualizer;
private pathNode!: InjectorTreeNode;
@ -56,10 +62,13 @@ export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
}
ngOnInit(): void {
this.injectorTree =
new InjectorTreeVisualizer(this.svgContainer.nativeElement, this.g.nativeElement, {
orientation: this.orientation,
});
this.injectorTree = new InjectorTreeVisualizer(
this.svgContainer.nativeElement,
this.g.nativeElement,
{
orientation: this.orientation,
},
);
if (this.pathNode) {
this.injectorTree.render(this.pathNode);

View file

@ -17,7 +17,7 @@ import {Theme, ThemeService} from '../theme-service';
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
import {TabUpdate} from './tab-update/index';
type Tabs = 'Components'|'Profiler'|'Router Tree'|'Injector Tree';
type Tabs = 'Components' | 'Profiler' | 'Router Tree' | 'Injector Tree';
@Component({
selector: 'ng-devtools-tabs',
@ -25,7 +25,7 @@ type Tabs = 'Components'|'Profiler'|'Router Tree'|'Injector Tree';
styleUrls: ['./devtools-tabs.component.scss'],
})
export class DevToolsTabsComponent implements OnInit, AfterViewInit {
@Input() angularVersion: string|undefined = undefined;
@Input() angularVersion: string | undefined = undefined;
@ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent;
@ViewChild('navBar', {static: true}) navbar!: MatTabNav;
@ -41,13 +41,14 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
routes: Route[] = [];
constructor(
public tabUpdate: TabUpdate,
public themeService: ThemeService,
private _messageBus: MessageBus<Events>,
private _applicationEnvironment: ApplicationEnvironment,
public tabUpdate: TabUpdate,
public themeService: ThemeService,
private _messageBus: MessageBus<Events>,
private _applicationEnvironment: ApplicationEnvironment,
) {
this.themeService.currentTheme.pipe(takeUntilDestroyed())
.subscribe((theme) => (this.currentTheme = theme));
this.themeService.currentTheme
.pipe(takeUntilDestroyed())
.subscribe((theme) => (this.currentTheme = theme));
this._messageBus.on('updateRouterTree', (routes) => {
this.routes = routes || [];
@ -99,7 +100,8 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
toggleTimingAPI(): void {
this.timingAPIEnabled = !this.timingAPIEnabled;
this.timingAPIEnabled ? this._messageBus.emit('enableTimingAPI') :
this._messageBus.emit('disableTimingAPI');
this.timingAPIEnabled
? this._messageBus.emit('enableTimingAPI')
: this._messageBus.emit('disableTimingAPI');
}
}

View file

@ -25,12 +25,19 @@ import {TabUpdate} from './tab-update/index';
@NgModule({
declarations: [DevToolsTabsComponent],
imports: [
MatTabsModule, MatIconModule, DirectiveExplorerModule, ProfilerModule, RouterTreeModule,
CommonModule, MatMenuModule, MatButtonModule, MatSlideToggleModule, MatTooltipModule,
InjectorTreeComponent
MatTabsModule,
MatIconModule,
DirectiveExplorerModule,
ProfilerModule,
RouterTreeModule,
CommonModule,
MatMenuModule,
MatButtonModule,
MatSlideToggleModule,
MatTooltipModule,
InjectorTreeComponent,
],
providers: [TabUpdate],
exports: [DevToolsTabsComponent],
})
export class DevToolsTabModule {
}
export class DevToolsTabModule {}

View file

@ -24,8 +24,7 @@ import {TabUpdate} from './tab-update/index';
selector: 'ng-directive-explorer',
template: '',
})
export class MockDirectiveExplorerComponent {
}
export class MockDirectiveExplorerComponent {}
describe('DevtoolsTabsComponent', () => {
let messageBusMock: MessageBus<Events>;

View file

@ -14,79 +14,82 @@ export interface MovedRecord {
previousIndex: number;
}
export const diff = <T>(differ: DefaultIterableDiffer<T>, a: T[], b: T[]):
{newItems: T[]; removedItems: T[]; movedItems: T[];} => {
differ.diff(a);
differ.diff(b);
export const diff = <T>(
differ: DefaultIterableDiffer<T>,
a: T[],
b: T[],
): {newItems: T[]; removedItems: T[]; movedItems: T[]} => {
differ.diff(a);
differ.diff(b);
const alreadySet: boolean[] = [];
const movedItems: T[] = [];
const alreadySet: boolean[] = [];
const movedItems: T[] = [];
// We first have to set the moved items to their correct positions.
// Keep in mind that the track by function may not guarantee
// that we haven't changed any of the items' props.
differ.forEachMovedItem(record => {
if (record.currentIndex === null) {
return;
}
if (record.previousIndex === null) {
return;
}
// We want to preserve the reference so that a default
// track by function used by the CDK, for instance, can
// recognize that this item's identity hasn't changed.
// At the same time, since we don't have the guarantee
// that we haven't already set the previousIndex while
// iterating, we need to check that. If we have, we assign
// this array item to a new object. We don't want to risk
// changing the properties of an object we'll use in the future.
if (!alreadySet[record.previousIndex]) {
a[record.currentIndex] = a[record.previousIndex];
} else {
a[record.currentIndex] = {} as unknown as T;
}
Object.keys(b[record.currentIndex] as unknown as {}).forEach(prop => {
// TypeScript's type inference didn't follow the check from above.
if (record.currentIndex === null) {
return;
}
(a[record.currentIndex] as any)[prop] = (b[record.currentIndex] as any)[prop];
});
if (!alreadySet[record.previousIndex]) {
// tslint:disable-next-line: no-non-null-assertion
a[record.previousIndex] = null!;
}
alreadySet[record.currentIndex] = true;
movedItems.push(a[record.currentIndex]);
});
// Now we can set the new items and remove the deleted ones.
const newItems: T[] = [];
const removedItems: T[] = [];
differ.forEachAddedItem(record => {
if (record.currentIndex !== null && record.previousIndex === null) {
a[record.currentIndex] = record.item;
alreadySet[record.currentIndex] = true;
newItems.push(record.item);
}
});
differ.forEachRemovedItem(record => {
if (record.previousIndex === null) {
return;
}
if (record.currentIndex === null && !alreadySet[record.previousIndex]) {
// tslint:disable-next-line: no-non-null-assertion
a[record.previousIndex] = null!;
}
removedItems.push(record.item);
});
for (let i = a.length - 1; i >= 0; i--) {
if (a[i] === null) {
a.splice(i, 1);
}
// We first have to set the moved items to their correct positions.
// Keep in mind that the track by function may not guarantee
// that we haven't changed any of the items' props.
differ.forEachMovedItem((record) => {
if (record.currentIndex === null) {
return;
}
if (record.previousIndex === null) {
return;
}
// We want to preserve the reference so that a default
// track by function used by the CDK, for instance, can
// recognize that this item's identity hasn't changed.
// At the same time, since we don't have the guarantee
// that we haven't already set the previousIndex while
// iterating, we need to check that. If we have, we assign
// this array item to a new object. We don't want to risk
// changing the properties of an object we'll use in the future.
if (!alreadySet[record.previousIndex]) {
a[record.currentIndex] = a[record.previousIndex];
} else {
a[record.currentIndex] = {} as unknown as T;
}
Object.keys(b[record.currentIndex] as unknown as {}).forEach((prop) => {
// TypeScript's type inference didn't follow the check from above.
if (record.currentIndex === null) {
return;
}
(a[record.currentIndex] as any)[prop] = (b[record.currentIndex] as any)[prop];
});
if (!alreadySet[record.previousIndex]) {
// tslint:disable-next-line: no-non-null-assertion
a[record.previousIndex] = null!;
}
alreadySet[record.currentIndex] = true;
movedItems.push(a[record.currentIndex]);
});
return {newItems, removedItems, movedItems};
};
// Now we can set the new items and remove the deleted ones.
const newItems: T[] = [];
const removedItems: T[] = [];
differ.forEachAddedItem((record) => {
if (record.currentIndex !== null && record.previousIndex === null) {
a[record.currentIndex] = record.item;
alreadySet[record.currentIndex] = true;
newItems.push(record.item);
}
});
differ.forEachRemovedItem((record) => {
if (record.previousIndex === null) {
return;
}
if (record.currentIndex === null && !alreadySet[record.previousIndex]) {
// tslint:disable-next-line: no-non-null-assertion
a[record.previousIndex] = null!;
}
removedItems.push(record.item);
});
for (let i = a.length - 1; i >= 0; i--) {
if (a[i] === null) {
a.splice(i, 1);
}
}
return {newItems, removedItems, movedItems};
};

View file

@ -6,8 +6,29 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild,} from '@angular/core';
import {ComponentExplorerView, ComponentExplorerViewQuery, DevToolsNode, DirectivePosition, ElementPosition, Events, MessageBus, PropertyQuery, PropertyQueryTypes,} from 'protocol';
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
NgZone,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import {
ComponentExplorerView,
ComponentExplorerViewQuery,
DevToolsNode,
DirectivePosition,
ElementPosition,
Events,
MessageBus,
PropertyQuery,
PropertyQueryTypes,
} from 'protocol';
import {SplitComponent} from '../../../lib/vendor/angular-split/public_api';
import {ApplicationOperations} from '../../application-operations/index';
@ -17,7 +38,10 @@ import {FlatNode} from './directive-forest/component-data-source';
import {DirectiveForestComponent} from './directive-forest/directive-forest.component';
import {IndexedNode} from './directive-forest/index-forest';
import {constructPathOfKeysToPropertyValue} from './property-resolver/directive-property-resolver';
import {ElementPropertyResolver, FlatNode as PropertyFlatNode} from './property-resolver/element-property-resolver';
import {
ElementPropertyResolver,
FlatNode as PropertyFlatNode,
} from './property-resolver/element-property-resolver';
const sameDirectives = (a: IndexedNode, b: IndexedNode) => {
if ((a.component && !b.component) || (!a.component && b.component)) {
@ -56,32 +80,37 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
@ViewChild('directiveForestSplitArea', {static: true, read: ElementRef})
directiveForestSplitArea!: ElementRef;
currentSelectedElement: IndexedNode|null = null;
currentSelectedElement: IndexedNode | null = null;
forest!: DevToolsNode[];
splitDirection: 'horizontal'|'vertical' = 'horizontal';
parents: FlatNode[]|null = null;
splitDirection: 'horizontal' | 'vertical' = 'horizontal';
parents: FlatNode[] | null = null;
private _resizeObserver = new ResizeObserver((entries) => this._ngZone.run(() => {
const resizedEntry = entries[0];
private _resizeObserver = new ResizeObserver((entries) =>
this._ngZone.run(() => {
const resizedEntry = entries[0];
if (resizedEntry.target === this.splitElementRef.nativeElement) {
this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal';
}
if (resizedEntry.target === this.splitElementRef.nativeElement) {
this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal';
}
if (!this.breadcrumbs) {
return;
}
if (!this.breadcrumbs) {
return;
}
this.breadcrumbs.updateScrollButtonVisibility();
}));
this.breadcrumbs.updateScrollButtonVisibility();
}),
);
private _clickedElement: IndexedNode|null = null;
private _refreshRetryTimeout: null|ReturnType<typeof setTimeout> = null;
private _clickedElement: IndexedNode | null = null;
private _refreshRetryTimeout: null | ReturnType<typeof setTimeout> = null;
constructor(
private _appOperations: ApplicationOperations, private _messageBus: MessageBus<Events>,
private _propResolver: ElementPropertyResolver, private _cdr: ChangeDetectorRef,
private _ngZone: NgZone) {}
private _appOperations: ApplicationOperations,
private _messageBus: MessageBus<Events>,
private _propResolver: ElementPropertyResolver,
private _cdr: ChangeDetectorRef,
private _ngZone: NgZone,
) {}
ngOnInit(): void {
this.subscribeToBackendEvents();
@ -95,7 +124,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
this._resizeObserver.unobserve(this.directiveForestSplitArea.nativeElement);
}
handleNodeSelection(node: IndexedNode|null): void {
handleNodeSelection(node: IndexedNode | null): void {
if (node) {
// We want to guarantee that we're not reusing any of the previous properties.
// That's possible if the user has selected an NgForOf and after that
@ -125,8 +154,9 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
}
refresh(): void {
const success =
this._messageBus.emit('getLatestComponentExplorerView', [this._constructViewQuery()]);
const success = this._messageBus.emit('getLatestComponentExplorerView', [
this._constructViewQuery(),
]);
// If the event was not throttled, we no longer need to retry.
if (success) {
this._refreshRetryTimeout && clearTimeout(this._refreshRetryTimeout);
@ -147,7 +177,8 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
}
const directiveIndex = this.currentSelectedElement.directives.findIndex(
directive => directive.name === directiveName);
(directive) => directive.name === directiveName,
);
if (directiveIndex === -1) {
// view the component definition
@ -174,7 +205,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
this._messageBus.emit('removeHighlightOverlay');
}
private _constructViewQuery(): ComponentExplorerViewQuery|undefined {
private _constructViewQuery(): ComponentExplorerViewQuery | undefined {
if (!this._clickedElement) {
return;
}
@ -189,8 +220,11 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
// We check if we're dealing with the same instance (i.e., if we have the same
// set of directives and component on it), if we do, we want to get the same
// set of properties which are already expanded.
if (!this._clickedElement || !this.currentSelectedElement ||
!sameDirectives(this._clickedElement, this.currentSelectedElement)) {
if (
!this._clickedElement ||
!this.currentSelectedElement ||
!sameDirectives(this._clickedElement, this.currentSelectedElement)
) {
return {
type: PropertyQueryTypes.All,
};
@ -213,13 +247,18 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
this.directiveForest.handleSelect(node);
}
handleSetParents(parents: FlatNode[]|null): void {
handleSetParents(parents: FlatNode[] | null): void {
this.parents = parents;
this._cdr.detectChanges();
}
inspect({node, directivePosition}:
{node: PropertyFlatNode; directivePosition: DirectivePosition}): void {
inspect({
node,
directivePosition,
}: {
node: PropertyFlatNode;
directivePosition: DirectivePosition;
}): void {
const objectPath = constructPathOfKeysToPropertyValue(node.prop);
this._appOperations.inspect(directivePosition, objectPath);
}

View file

@ -39,5 +39,4 @@ import {PropertyTabModule} from './property-tab/property-tab.module';
MatTooltipModule,
],
})
export class DirectiveExplorerModule {
}
export class DirectiveExplorerModule {}

View file

@ -10,9 +10,9 @@ import {PropertyQueryTypes} from 'protocol';
import {DirectiveExplorerComponent} from './directive-explorer.component';
import {IndexedNode} from './directive-forest/index-forest';
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
import SpyObj = jasmine.SpyObj;
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
describe('DirectiveExplorerComponent', () => {
let messageBusMock: any;
@ -22,14 +22,20 @@ describe('DirectiveExplorerComponent', () => {
let ngZone: any;
beforeEach(() => {
applicationOperationsSpy =
jasmine.createSpyObj('_appOperations', ['viewSource', 'selectDomElement']);
applicationOperationsSpy = jasmine.createSpyObj('_appOperations', [
'viewSource',
'selectDomElement',
]);
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
cdr = jasmine.createSpyObj('_cdr', ['detectChanges']);
ngZone = jasmine.createSpyObj('_ngZone', ['run']);
comp = new DirectiveExplorerComponent(
applicationOperationsSpy, messageBusMock, new ElementPropertyResolver(messageBusMock), cdr,
ngZone);
applicationOperationsSpy,
messageBusMock,
new ElementPropertyResolver(messageBusMock),
cdr,
ngZone,
);
});
it('should create instance from class', () => {
@ -39,8 +45,10 @@ describe('DirectiveExplorerComponent', () => {
it('subscribe to backend events', () => {
comp.subscribeToBackendEvents();
expect(messageBusMock.on).toHaveBeenCalledTimes(2);
expect(messageBusMock.on)
.toHaveBeenCalledWith('latestComponentExplorerView', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith(
'latestComponentExplorerView',
jasmine.any(Function),
);
expect(messageBusMock.on).toHaveBeenCalledWith('componentTreeDirty', jasmine.any(Function));
});
@ -53,23 +61,24 @@ describe('DirectiveExplorerComponent', () => {
it('should emit getLatestComponentExplorerView event with null view query', () => {
comp.refresh();
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
undefined
undefined,
]);
});
it('should emit getLatestComponentExplorerView event on refresh with view query no properties',
() => {
const currentSelectedElement =
jasmine.createSpyObj('currentSelectedElement', ['position', 'children']);
currentSelectedElement.position = [0];
currentSelectedElement.children = [];
comp.currentSelectedElement = currentSelectedElement;
comp.refresh();
expect(comp.currentSelectedElement).toBeTruthy();
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
undefined
]);
});
it('should emit getLatestComponentExplorerView event on refresh with view query no properties', () => {
const currentSelectedElement = jasmine.createSpyObj('currentSelectedElement', [
'position',
'children',
]);
currentSelectedElement.position = [0];
currentSelectedElement.children = [];
comp.currentSelectedElement = currentSelectedElement;
comp.refresh();
expect(comp.currentSelectedElement).toBeTruthy();
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
undefined,
]);
});
});
describe('node selection event', () => {

View file

@ -6,7 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, ViewChild,} from '@angular/core';
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
@ -31,8 +42,9 @@ export class BreadcrumbsComponent implements OnInit, AfterViewInit, OnChanges {
updateScrollButtonVisibility$ = new Subject<void>();
ngOnInit(): void {
this.updateScrollButtonVisibility$.pipe(debounceTime(100))
.subscribe(() => this.updateScrollButtonVisibility());
this.updateScrollButtonVisibility$
.pipe(debounceTime(100))
.subscribe(() => this.updateScrollButtonVisibility());
}
ngAfterViewInit(): void {

View file

@ -19,5 +19,4 @@ import {BreadcrumbsComponent} from './breadcrumbs.component';
imports: [CommonModule, MatCardModule, MatButtonModule, MatIconModule],
exports: [BreadcrumbsComponent],
})
export class BreadcrumbsModule {
}
export class BreadcrumbsModule {}

View file

@ -183,8 +183,10 @@ const tree4: DevToolsNode = {
describe('ComponentDataSource', () => {
let dataSource: ComponentDataSource;
const treeControl =
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
const treeControl = new FlatTreeControl<FlatNode>(
(node) => node.level,
(node) => node.expandable,
);
beforeEach(() => (dataSource = new ComponentDataSource(treeControl)));

View file

@ -32,16 +32,18 @@ export interface FlatNode {
const expandable = (node: IndexedNode) => !!node.children && node.children.length > 0;
const trackBy: TrackByFunction<FlatNode> = (_: number, item: FlatNode) =>
`${item.id}#${item.expandable}`;
`${item.id}#${item.expandable}`;
const getId = (node: IndexedNode) => {
let prefix = '';
if (node.component) {
prefix = node.component.id.toString();
}
const dirIds = node.directives.map((d) => d.id).sort((a, b) => {
return a - b;
});
const dirIds = node.directives
.map((d) => d.id)
.sort((a, b) => {
return a - b;
});
return prefix + '-' + dirIds.join('-');
};
@ -75,28 +77,28 @@ export class ComponentDataSource extends DataSource<FlatNode> {
private _nodeToFlat = new WeakMap<IndexedNode, FlatNode>();
private _treeFlattener = new MatTreeFlattener(
(node: IndexedNode, level: number) => {
if (this._nodeToFlat.has(node)) {
return this._nodeToFlat.get(node);
}
const flatNode: FlatNode = {
expandable: expandable(node),
id: getId(node),
// We can compare the nodes in the navigation functions above
// based on this identifier directly, since it's a reference type
// and the reference is preserved after transformation.
position: node.position,
name: node.component ? node.component.name : node.element,
directives: node.directives.map((d) => d.name).join(', '),
original: node,
level,
};
this._nodeToFlat.set(node, flatNode);
return flatNode;
},
(node) => (node ? node.level : -1),
(node) => (node ? node.expandable : false),
(node) => (node ? node.children : []),
(node: IndexedNode, level: number) => {
if (this._nodeToFlat.has(node)) {
return this._nodeToFlat.get(node);
}
const flatNode: FlatNode = {
expandable: expandable(node),
id: getId(node),
// We can compare the nodes in the navigation functions above
// based on this identifier directly, since it's a reference type
// and the reference is preserved after transformation.
position: node.position,
name: node.component ? node.component.name : node.element,
directives: node.directives.map((d) => d.name).join(', '),
original: node,
level,
};
this._nodeToFlat.set(node, flatNode);
return flatNode;
},
(node) => (node ? node.level : -1),
(node) => (node ? node.expandable : false),
(node) => (node ? node.children : []),
);
constructor(private _treeControl: FlatTreeControl<FlatNode>) {
@ -111,14 +113,14 @@ export class ComponentDataSource extends DataSource<FlatNode> {
return this._expandedData.value;
}
getFlatNodeFromIndexedNode(indexedNode: IndexedNode): FlatNode|undefined {
getFlatNodeFromIndexedNode(indexedNode: IndexedNode): FlatNode | undefined {
return this._nodeToFlat.get(indexedNode);
}
update(
forest: DevToolsNode[],
showCommentNodes: boolean,
): {newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[]} {
forest: DevToolsNode[],
showCommentNodes: boolean,
): {newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[]} {
if (!forest) {
return {newItems: [], movedItems: [], removedItems: []};
}
@ -151,9 +153,9 @@ export class ComponentDataSource extends DataSource<FlatNode> {
});
const {newItems, movedItems, removedItems} = diff<FlatNode>(
this._differ,
this.data,
flattenedCollection,
this._differ,
this.data,
flattenedCollection,
);
this._treeControl.dataNodes = this.data;
this._flattenedData.next(this.data);
@ -176,18 +178,17 @@ export class ComponentDataSource extends DataSource<FlatNode> {
this._treeControl.expansionModel.changed,
this._flattenedData,
];
return merge<unknown[]>(...changes)
.pipe(
map(() => {
this._expandedData.next(
this._treeFlattener.expandFlattenedNodes(
this.data,
this._treeControl as FlatTreeControl<FlatNode|undefined>,
) as FlatNode[],
);
return this._expandedData.value;
}),
return merge<unknown[]>(...changes).pipe(
map(() => {
this._expandedData.next(
this._treeFlattener.expandFlattenedNodes(
this.data,
this._treeControl as FlatTreeControl<FlatNode | undefined>,
) as FlatNode[],
);
return this._expandedData.value;
}),
);
}
override disconnect(): void {}

View file

@ -22,13 +22,16 @@ export const isChildOf = (childPosition: number[], parentPosition: number[]) =>
return true;
};
export const parentCollapsed =
(nodeIdx: number, all: FlatNode[], treeControl: FlatTreeControl<FlatNode>) => {
const node = all[nodeIdx];
for (let i = nodeIdx - 1; i >= 0; i--) {
if (isChildOf(node.position, all[i].position) && !treeControl.isExpanded(all[i])) {
return true;
}
}
return false;
};
export const parentCollapsed = (
nodeIdx: number,
all: FlatNode[],
treeControl: FlatTreeControl<FlatNode>,
) => {
const node = all[nodeIdx];
for (let i = nodeIdx - 1; i >= 0; i--) {
if (isChildOf(node.position, all[i].position) && !treeControl.isExpanded(all[i])) {
return true;
}
}
return false;
};

View file

@ -8,7 +8,18 @@
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {FlatTreeControl} from '@angular/cdk/tree';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, ViewChild,} from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {DevToolsNode, ElementPosition, Events, MessageBus} from 'protocol';
@ -30,7 +41,7 @@ export class DirectiveForestComponent {
this._latestForest = forest;
const result = this._updateForest(forest);
const changed =
result.movedItems.length || result.newItems.length || result.removedItems.length;
result.movedItems.length || result.newItems.length || result.removedItems.length;
if (this.currentSelectedElement && changed) {
this._reselectNodeOnUpdate();
}
@ -42,9 +53,9 @@ export class DirectiveForestComponent {
this.forest = this._latestForest;
}
@Output() selectNode = new EventEmitter<IndexedNode|null>();
@Output() selectNode = new EventEmitter<IndexedNode | null>();
@Output() selectDomElement = new EventEmitter<IndexedNode>();
@Output() setParents = new EventEmitter<FlatNode[]|null>();
@Output() setParents = new EventEmitter<FlatNode[] | null>();
@Output() highlightComponent = new EventEmitter<ElementPosition>();
@Output() removeComponentHighlight = new EventEmitter<void>();
@Output() toggleInspector = new EventEmitter<void>();
@ -54,28 +65,32 @@ export class DirectiveForestComponent {
filterRegex = new RegExp('.^');
currentlyMatchedIndex = -1;
selectedNode: FlatNode|null = null;
selectedNode: FlatNode | null = null;
parents!: FlatNode[];
private _highlightIDinTreeFromElement: number|null = null;
private _highlightIDinTreeFromElement: number | null = null;
private _showCommentNodes = false;
private _latestForest!: DevToolsNode[];
set highlightIDinTreeFromElement(id: number|null) {
set highlightIDinTreeFromElement(id: number | null) {
this._highlightIDinTreeFromElement = id;
this._cdr.markForCheck();
}
readonly treeControl =
new FlatTreeControl<FlatNode>((node) => node!.level, (node) => node.expandable);
readonly treeControl = new FlatTreeControl<FlatNode>(
(node) => node!.level,
(node) => node.expandable,
);
readonly dataSource = new ComponentDataSource(this.treeControl);
readonly itemHeight = 18;
private _initialized = false;
constructor(
private _tabUpdate: TabUpdate, private _messageBus: MessageBus<Events>,
private _cdr: ChangeDetectorRef) {
private _tabUpdate: TabUpdate,
private _messageBus: MessageBus<Events>,
private _cdr: ChangeDetectorRef,
) {
this.subscribeToInspectorEvents();
this._tabUpdate.tabUpdate$.pipe(takeUntilDestroyed()).subscribe(() => {
if (this.viewport) {
@ -111,8 +126,9 @@ export class DirectiveForestComponent {
}
handleSelect(node: FlatNode): void {
this.currentlyMatchedIndex =
this.dataSource.data.findIndex((matchedNode) => matchedNode.id === node.id);
this.currentlyMatchedIndex = this.dataSource.data.findIndex(
(matchedNode) => matchedNode.id === node.id,
);
this.selectAndEnsureVisible(node);
}
@ -156,8 +172,9 @@ export class DirectiveForestComponent {
}
private _reselectNodeOnUpdate(): void {
const nodeThatStillExists =
this.dataSource.getFlatNodeFromIndexedNode(this.currentSelectedElement);
const nodeThatStillExists = this.dataSource.getFlatNodeFromIndexedNode(
this.currentSelectedElement,
);
if (nodeThatStillExists) {
this.select(nodeThatStillExists);
} else {
@ -165,8 +182,11 @@ export class DirectiveForestComponent {
}
}
private _updateForest(forest: DevToolsNode[]):
{newItems: FlatNode[]; movedItems: FlatNode[]; removedItems: FlatNode[];} {
private _updateForest(forest: DevToolsNode[]): {
newItems: FlatNode[];
movedItems: FlatNode[];
removedItems: FlatNode[];
} {
const result = this.dataSource.update(forest, this._showCommentNodes);
if (!this._initialized && forest && forest.length) {
this.treeControl.expandAll();
@ -184,8 +204,9 @@ export class DirectiveForestComponent {
this.parents = [];
for (let i = 1; i <= position.length; i++) {
const current = position.slice(0, i);
const selectedNode =
this.dataSource.data.find((item) => item.position.toString() === current.toString());
const selectedNode = this.dataSource.data.find(
(item) => item.position.toString() === current.toString(),
);
// We might not be able to find the parent if the user has hidden the comment nodes.
if (selectedNode) {
@ -279,8 +300,10 @@ export class DirectiveForestComponent {
}
isMatched(node: FlatNode): boolean {
return this.filterRegex.test(node.name.toLowerCase()) ||
this.filterRegex.test(node.directives.toLowerCase());
return (
this.filterRegex.test(node.name.toLowerCase()) ||
this.filterRegex.test(node.directives.toLowerCase())
);
}
handleFilter(filterText: string): void {
@ -324,8 +347,9 @@ export class DirectiveForestComponent {
prevMatched(): void {
const indexesOfMatchedNodes = this._findMatchedNodes();
this.currentlyMatchedIndex = (this.currentlyMatchedIndex - 1 + indexesOfMatchedNodes.length) %
indexesOfMatchedNodes.length;
this.currentlyMatchedIndex =
(this.currentlyMatchedIndex - 1 + indexesOfMatchedNodes.length) %
indexesOfMatchedNodes.length;
const indexToSelect = indexesOfMatchedNodes[this.currentlyMatchedIndex];
const nodeToSelect = this.dataSource.data[indexToSelect];
if (indexToSelect !== undefined) {
@ -352,11 +376,13 @@ export class DirectiveForestComponent {
}
isHighlighted(node: FlatNode): boolean {
return !!this._highlightIDinTreeFromElement &&
this._highlightIDinTreeFromElement === node.original.component?.id;
return (
!!this._highlightIDinTreeFromElement &&
this._highlightIDinTreeFromElement === node.original.component?.id
);
}
isElement(node: FlatNode): boolean|null {
isElement(node: FlatNode): boolean | null {
return node.original.component && node.original.component.isElement;
}
}

View file

@ -30,5 +30,4 @@ import {FilterModule} from './filter/filter.module';
],
exports: [DirectiveForestComponent, BreadcrumbsModule],
})
export class DirectiveForestModule {
}
export class DirectiveForestModule {}

View file

@ -19,5 +19,4 @@ import {FilterComponent} from './filter.component';
imports: [CommonModule, MatCardModule, MatIconModule, MatButtonModule],
exports: [FilterComponent],
})
export class FilterModule {
}
export class FilterModule {}

View file

@ -14,10 +14,83 @@ describe('indexForest', () => {
});
it('should index a forest', () => {
expect(indexForest([
expect(
indexForest([
{
element: 'Parent1',
directives: [],
component: {
isElement: false,
name: 'Cmp1',
id: 1,
},
children: [
{
element: 'Child1_1',
directives: [
{
name: 'Dir1',
id: 1,
},
{
name: 'Dir2',
id: 1,
},
],
component: null,
children: [],
},
{
element: 'Child1_2',
directives: [],
component: {
isElement: false,
name: 'Cmp2',
id: 1,
},
children: [],
},
],
},
{
element: 'Parent2',
directives: [],
component: null,
children: [
{
element: 'Child2_1',
directives: [
{
name: 'Dir3',
id: 1,
},
],
component: null,
children: [],
},
{
element: 'Child2_2',
directives: [
{
name: 'Dir4',
id: 1,
},
{
name: 'Dir5',
id: 1,
},
],
component: null,
children: [],
},
],
},
]),
).toEqual([
{
element: 'Parent1',
directives: [],
position: [0],
component: {
isElement: false,
name: 'Cmp1',
@ -26,6 +99,7 @@ describe('indexForest', () => {
children: [
{
element: 'Child1_1',
position: [0, 0],
directives: [
{
name: 'Dir1',
@ -42,6 +116,7 @@ describe('indexForest', () => {
{
element: 'Child1_2',
directives: [],
position: [0, 1],
component: {
isElement: false,
name: 'Cmp2',
@ -55,9 +130,11 @@ describe('indexForest', () => {
element: 'Parent2',
directives: [],
component: null,
position: [1],
children: [
{
element: 'Child2_1',
position: [1, 0],
directives: [
{
name: 'Dir3',
@ -69,6 +146,7 @@ describe('indexForest', () => {
},
{
element: 'Child2_2',
position: [1, 1],
directives: [
{
name: 'Dir4',
@ -84,83 +162,6 @@ describe('indexForest', () => {
},
],
},
]))
.toEqual([
{
element: 'Parent1',
directives: [],
position: [0],
component: {
isElement: false,
name: 'Cmp1',
id: 1,
},
children: [
{
element: 'Child1_1',
position: [0, 0],
directives: [
{
name: 'Dir1',
id: 1,
},
{
name: 'Dir2',
id: 1,
},
],
component: null,
children: [],
},
{
element: 'Child1_2',
directives: [],
position: [0, 1],
component: {
isElement: false,
name: 'Cmp2',
id: 1,
},
children: [],
},
],
},
{
element: 'Parent2',
directives: [],
component: null,
position: [1],
children: [
{
element: 'Child2_1',
position: [1, 0],
directives: [
{
name: 'Dir3',
id: 1,
},
],
component: null,
children: [],
},
{
element: 'Child2_2',
position: [1, 1],
directives: [
{
name: 'Dir4',
id: 1,
},
{
name: 'Dir5',
id: 1,
},
],
component: null,
children: [],
},
],
},
]);
]);
});
});

View file

@ -13,16 +13,19 @@ export interface IndexedNode extends DevToolsNode {
children: IndexedNode[];
}
const indexTree =
(node: DevToolsNode, idx: number, parentPosition: ElementPosition = []): IndexedNode => {
const position = parentPosition.concat([idx]);
return {
position,
element: node.element,
component: node.component,
directives: node.directives.map((d, i) => ({name: d.name, id: d.id})),
children: node.children.map((n, i) => indexTree(n, i, position)),
} as IndexedNode;
};
const indexTree = (
node: DevToolsNode,
idx: number,
parentPosition: ElementPosition = [],
): IndexedNode => {
const position = parentPosition.concat([idx]);
return {
position,
element: node.element,
component: node.component,
directives: node.directives.map((d, i) => ({name: d.name, id: d.id})),
children: node.children.map((n, i) => indexTree(n, i, position)),
} as IndexedNode;
};
export const indexForest = (forest: DevToolsNode[]) => forest.map((n, i) => indexTree(n, i));

View file

@ -10,17 +10,19 @@ import {Descriptor} from 'protocol';
import {Property} from './element-property-resolver';
export const arrayifyProps =
(props: {[prop: string]: Descriptor}|Descriptor[], parent: Property|null = null): Property[] =>
Object.entries(props)
.map(([name, val]) => ({name, descriptor: val, parent}))
.sort((a, b) => {
const parsedA = parseInt(a.name, 10);
const parsedB = parseInt(b.name, 10);
export const arrayifyProps = (
props: {[prop: string]: Descriptor} | Descriptor[],
parent: Property | null = null,
): Property[] =>
Object.entries(props)
.map(([name, val]) => ({name, descriptor: val, parent}))
.sort((a, b) => {
const parsedA = parseInt(a.name, 10);
const parsedB = parseInt(b.name, 10);
if (isNaN(parsedA) || isNaN(parsedB)) {
return a.name > b.name ? 1 : -1;
}
if (isNaN(parsedA) || isNaN(parsedB)) {
return a.name > b.name ? 1 : -1;
}
return parsedA - parsedB;
});
return parsedA - parsedB;
});

View file

@ -8,7 +8,15 @@
import {FlatTreeControl} from '@angular/cdk/tree';
import {ViewEncapsulation} from '@angular/core';
import {Descriptor, DirectiveMetadata, DirectivePosition, Events, MessageBus, NestedProp, Properties} from 'protocol';
import {
Descriptor,
DirectiveMetadata,
DirectivePosition,
Events,
MessageBus,
NestedProp,
Properties,
} from 'protocol';
import {FlatNode, Property} from './element-property-resolver';
import {getTreeFlattener} from './flatten';
@ -20,38 +28,45 @@ export interface DirectiveTreeData {
treeControl: FlatTreeControl<FlatNode>;
}
const getDirectiveControls = (dataSource: PropertyDataSource):
{dataSource: PropertyDataSource; treeControl: FlatTreeControl<FlatNode>} => {
const treeControl = dataSource.treeControl;
return {
dataSource,
treeControl,
};
};
const getDirectiveControls = (
dataSource: PropertyDataSource,
): {dataSource: PropertyDataSource; treeControl: FlatTreeControl<FlatNode>} => {
const treeControl = dataSource.treeControl;
return {
dataSource,
treeControl,
};
};
export const constructPathOfKeysToPropertyValue =
(nodePropToGetKeysFor: Property, keys: string[] = []): string[] => {
keys.unshift(nodePropToGetKeysFor.name);
const parentNodeProp = nodePropToGetKeysFor.parent;
if (parentNodeProp) {
constructPathOfKeysToPropertyValue(parentNodeProp, keys);
}
return keys;
};
export const constructPathOfKeysToPropertyValue = (
nodePropToGetKeysFor: Property,
keys: string[] = [],
): string[] => {
keys.unshift(nodePropToGetKeysFor.name);
const parentNodeProp = nodePropToGetKeysFor.parent;
if (parentNodeProp) {
constructPathOfKeysToPropertyValue(parentNodeProp, keys);
}
return keys;
};
export class DirectivePropertyResolver {
private _treeFlattener = getTreeFlattener();
private _treeControl =
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
private _treeControl = new FlatTreeControl<FlatNode>(
(node) => node.level,
(node) => node.expandable,
);
private _inputsDataSource: PropertyDataSource;
private _outputsDataSource: PropertyDataSource;
private _stateDataSource: PropertyDataSource;
constructor(
private _messageBus: MessageBus<Events>, private _props: Properties,
private _directivePosition: DirectivePosition) {
private _messageBus: MessageBus<Events>,
private _props: Properties,
private _directivePosition: DirectivePosition,
) {
const {inputProps, outputProps, stateProps} = this._classifyProperties();
this._inputsDataSource = this._createDataSourceFromProps(inputProps);
@ -71,7 +86,7 @@ export class DirectivePropertyResolver {
return getDirectiveControls(this._stateDataSource);
}
get directiveMetadata(): DirectiveMetadata|undefined {
get directiveMetadata(): DirectiveMetadata | undefined {
return this._props.metadata;
}
@ -83,11 +98,11 @@ export class DirectivePropertyResolver {
return this._directivePosition;
}
get directiveViewEncapsulation(): ViewEncapsulation|undefined {
get directiveViewEncapsulation(): ViewEncapsulation | undefined {
return this._props.metadata?.encapsulation;
}
get directiveHasOnPushStrategy(): boolean|undefined {
get directiveHasOnPushStrategy(): boolean | undefined {
return this._props.metadata?.onPush;
}
@ -117,11 +132,17 @@ export class DirectivePropertyResolver {
private _createDataSourceFromProps(props: {[name: string]: Descriptor}): PropertyDataSource {
return new PropertyDataSource(
props, this._treeFlattener, this._treeControl, this._directivePosition, this._messageBus);
props,
this._treeFlattener,
this._treeControl,
this._directivePosition,
this._messageBus,
);
}
private _classifyProperties(): {
inputProps: {[name: string]: Descriptor}; outputProps: {[name: string]: Descriptor};
inputProps: {[name: string]: Descriptor};
outputProps: {[name: string]: Descriptor};
stateProps: {[name: string]: Descriptor};
} {
const inputLabels: Set<string> = new Set(Object.values(this._props.metadata?.inputs || {}));
@ -133,9 +154,11 @@ export class DirectivePropertyResolver {
let propPointer: {[name: string]: Descriptor};
Object.keys(this.directiveProperties).forEach((propName) => {
propPointer = inputLabels.has(propName) ? inputProps :
outputLabels.has(propName) ? outputProps :
stateProps;
propPointer = inputLabels.has(propName)
? inputProps
: outputLabels.has(propName)
? outputProps
: stateProps;
propPointer[propName] = this.directiveProperties[propName];
});

View file

@ -7,7 +7,14 @@
*/
import {Injectable} from '@angular/core';
import {ComponentExplorerViewProperties, Descriptor, DirectivePosition, DirectivesProperties, Events, MessageBus,} from 'protocol';
import {
ComponentExplorerViewProperties,
Descriptor,
DirectivePosition,
DirectivesProperties,
Events,
MessageBus,
} from 'protocol';
import {IndexedNode} from '../directive-forest/index-forest';
@ -22,7 +29,7 @@ export interface FlatNode {
export interface Property {
name: string;
descriptor: Descriptor;
parent: Property|null;
parent: Property | null;
}
@Injectable()
@ -52,7 +59,9 @@ export class ElementPropertyResolver {
position.directive = indexedNode.directives.findIndex((d) => d.name === key);
}
this._directivePropertiesController.set(
key, new DirectivePropertyResolver(this._messageBus, data[key], position));
key,
new DirectivePropertyResolver(this._messageBus, data[key], position),
);
});
}
@ -79,7 +88,7 @@ export class ElementPropertyResolver {
return result;
}
getDirectiveController(directive: string): DirectivePropertyResolver|undefined {
getDirectiveController(directive: string): DirectivePropertyResolver | undefined {
return this._directivePropertiesController.get(directive);
}
}

View file

@ -14,13 +14,18 @@ import {arrayifyProps} from './arrayify-props';
import {FlatNode, Property} from './element-property-resolver';
export const getTreeFlattener = () =>
new MatTreeFlattener((node: Property, level: number): FlatNode => {
new MatTreeFlattener(
(node: Property, level: number): FlatNode => {
return {
expandable: expandable(node.descriptor),
prop: node,
level,
};
}, (node) => node.level, (node) => node.expandable, (node) => getChildren(node));
},
(node) => node.level,
(node) => node.expandable,
(node) => getChildren(node),
);
export const expandable = (prop: Descriptor) => {
if (!prop) {
@ -32,10 +37,12 @@ export const expandable = (prop: Descriptor) => {
return !(prop.type !== PropType.Object && prop.type !== PropType.Array);
};
const getChildren = (prop: Property): Property[]|undefined => {
const getChildren = (prop: Property): Property[] | undefined => {
const descriptor = prop.descriptor;
if ((descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
!(descriptor.value instanceof Observable)) {
if (
(descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
!(descriptor.value instanceof Observable)
) {
return arrayifyProps(descriptor.value || {}, prop);
}
console.error('Unexpected data type', descriptor, 'in property', prop);

View file

@ -13,23 +13,29 @@ import {FlatNode} from './element-property-resolver';
import {getTreeFlattener} from './flatten';
import {PropertyDataSource} from './property-data-source';
const flatTreeControl =
new FlatTreeControl<FlatNode>((node) => node.level, (node) => node.expandable);
const flatTreeControl = new FlatTreeControl<FlatNode>(
(node) => node.level,
(node) => node.expandable,
);
describe('PropertyDataSource', () => {
it('should detect changes in the collection', () => {
const source = new PropertyDataSource(
{
foo: {
editable: true,
expandable: false,
preview: '42',
type: PropType.Number,
value: 42,
containerType: null,
},
{
foo: {
editable: true,
expandable: false,
preview: '42',
type: PropType.Number,
value: 42,
containerType: null,
},
getTreeFlattener(), flatTreeControl, {element: [1, 2, 3]}, null as any);
},
getTreeFlattener(),
flatTreeControl,
{element: [1, 2, 3]},
null as any,
);
source.update({
foo: {

View file

@ -20,7 +20,7 @@ import {arrayifyProps} from './arrayify-props';
import {FlatNode, Property} from './element-property-resolver';
const trackBy: TrackByFunction<FlatNode> = (_: number, item: FlatNode) =>
`#${item.prop.name}#${item.prop.descriptor.preview}#${item.level}`;
`#${item.prop.name}#${item.prop.descriptor.preview}#${item.level}`;
export class PropertyDataSource extends DataSource<FlatNode> {
private _data = new BehaviorSubject<FlatNode[]>([]);
@ -29,10 +29,12 @@ export class PropertyDataSource extends DataSource<FlatNode> {
private _differ = new DefaultIterableDiffer<FlatNode>(trackBy);
constructor(
props: {[prop: string]: Descriptor},
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
private _treeControl: FlatTreeControl<FlatNode>, private _entityPosition: DirectivePosition,
private _messageBus: MessageBus<Events>) {
props: {[prop: string]: Descriptor},
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
private _treeControl: FlatTreeControl<FlatNode>,
private _entityPosition: DirectivePosition,
private _messageBus: MessageBus<Events>,
) {
super();
this._data.next(this._treeFlattener.flattenNodes(arrayifyProps(props)));
}
@ -66,14 +68,20 @@ export class PropertyDataSource extends DataSource<FlatNode> {
});
this._subscriptions.push(s);
const changes =
[collectionViewer.viewChange, this._treeControl.expansionModel.changed, this._data];
const changes = [
collectionViewer.viewChange,
this._treeControl.expansionModel.changed,
this._data,
];
return merge<unknown[]>(...changes).pipe(map(() => {
this._expandedData.next(
this._treeFlattener.expandFlattenedNodes(this.data, this._treeControl));
return this._expandedData.value;
}));
return merge<unknown[]>(...changes).pipe(
map(() => {
this._expandedData.next(
this._treeFlattener.expandFlattenedNodes(this.data, this._treeControl),
);
return this._expandedData.value;
}),
);
}
override disconnect(): void {
@ -104,14 +112,16 @@ export class PropertyDataSource extends DataSource<FlatNode> {
this._messageBus.emit('getNestedProperties', [this._entityPosition, parentPath]);
this._messageBus.once(
'nestedProperties', (position: DirectivePosition, data: Properties, _path: string[]) => {
node.prop.descriptor.value = data.props;
this._treeControl.expand(node);
const props = arrayifyProps(data.props, node.prop);
const flatNodes = this._treeFlattener.flattenNodes(props);
flatNodes.forEach((f) => (f.level += node.level + 1));
this.data.splice(index + 1, 0, ...flatNodes);
this._data.next(this.data);
});
'nestedProperties',
(position: DirectivePosition, data: Properties, _path: string[]) => {
node.prop.descriptor.value = data.props;
this._treeControl.expand(node);
const props = arrayifyProps(data.props, node.prop);
const flatNodes = this._treeFlattener.flattenNodes(props);
flatNodes.forEach((f) => (f.level += node.level + 1));
this.data.splice(index + 1, 0, ...flatNodes);
this._data.next(this.data);
},
);
}
}

View file

@ -13,14 +13,14 @@ import {FlatNode} from './element-property-resolver';
export const getExpandedDirectiveProperties = (data: FlatNode[]): NestedProp[] => {
const getChildren = (prop: Descriptor) => {
if ((prop.type === PropType.Object || prop.type === PropType.Array) && prop.value) {
return Object.entries(prop.value).map(([k, v]: [
string, any
]): {name: number|string, children: NestedProp[]} => {
return {
name: prop.type === PropType.Array ? parseInt(k, 10) : k,
children: getChildren(v),
};
});
return Object.entries(prop.value).map(
([k, v]: [string, any]): {name: number | string; children: NestedProp[]} => {
return {
name: prop.type === PropType.Array ? parseInt(k, 10) : k,
children: getChildren(v),
};
},
);
}
return [];
};

View file

@ -25,14 +25,14 @@ export class ComponentMetadataComponent {
viewEncapsulationModes = ['Emulated', 'Native', 'None', 'ShadowDom'];
get controller(): DirectivePropertyResolver|undefined {
get controller(): DirectivePropertyResolver | undefined {
if (!this.currentSelectedComponent) {
return;
}
return this._nestedProps.getDirectiveController(this.currentSelectedComponent.name);
}
get viewEncapsulation(): string|undefined {
get viewEncapsulation(): string | undefined {
const encapsulationIndex = this?.controller?.directiveViewEncapsulation;
if (encapsulationIndex !== undefined) {
return this.viewEncapsulationModes[encapsulationIndex];
@ -40,7 +40,7 @@ export class ComponentMetadataComponent {
return undefined;
}
get changeDetectionStrategy(): string|undefined {
get changeDetectionStrategy(): string | undefined {
const onPush = this?.controller?.directiveHasOnPushStrategy;
return onPush ? 'OnPush' : onPush !== undefined ? 'Default' : undefined;
}

View file

@ -18,5 +18,5 @@ import {IndexedNode} from '../directive-forest/index-forest';
})
export class PropertyTabHeaderComponent {
@Input({required: true}) currentSelectedElement!: IndexedNode;
@Input() currentDirectives: string[]|undefined;
@Input() currentDirectives: string[] | undefined;
}

View file

@ -21,10 +21,13 @@ import {PropertyViewModule} from './property-view/property-view.module';
@NgModule({
declarations: [PropertyTabComponent, PropertyTabHeaderComponent, ComponentMetadataComponent],
imports: [
PropertyViewModule, CommonModule, MatButtonModule, MatExpansionModule, MatIconModule,
MatTooltipModule
PropertyViewModule,
CommonModule,
MatButtonModule,
MatExpansionModule,
MatIconModule,
MatTooltipModule,
],
exports: [PropertyTabComponent],
})
export class PropertyTabModule {
}
export class PropertyTabModule {}

View file

@ -6,11 +6,20 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output,} from '@angular/core';
import {
AfterViewChecked,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import {ContainerType} from 'protocol';
type EditorType = string|number|boolean;
type EditorResult = EditorType|Array<EditorType>;
type EditorType = string | number | boolean;
type EditorResult = EditorType | Array<EditorType>;
enum PropertyEditorState {
Read,
@ -43,7 +52,10 @@ export class PropertyEditorComponent implements AfterViewChecked, OnInit {
valueToSubmit!: EditorResult;
currentPropertyState = this.readState;
constructor(private _cd: ChangeDetectorRef, private _elementRef: ElementRef) {}
constructor(
private _cd: ChangeDetectorRef,
private _elementRef: ElementRef,
) {}
ngOnInit(): void {
this.valueToSubmit = this.initialValue;

View file

@ -21,7 +21,9 @@ export class PropertyPreviewComponent {
@Output() inspect = new EventEmitter<void>();
get isClickableProp(): boolean {
return this.node.prop.descriptor.type === PropType.Function ||
this.node.prop.descriptor.type === PropType.HTMLNode;
return (
this.node.prop.descriptor.type === PropType.Function ||
this.node.prop.descriptor.type === PropType.HTMLNode
);
}
}

View file

@ -18,11 +18,11 @@ import {FlatNode} from '../../property-resolver/element-property-resolver';
styleUrls: ['./property-tab-body.component.scss'],
})
export class PropertyTabBodyComponent {
@Input({required: true}) currentSelectedElement!: IndexedNode|null;
@Input({required: true}) currentSelectedElement!: IndexedNode | null;
@Output() inspect = new EventEmitter<{node: FlatNode; directivePosition: DirectivePosition}>();
@Output() viewSource = new EventEmitter<string>();
getCurrentDirectives(): string[]|undefined {
getCurrentDirectives(): string[] | undefined {
if (!this.currentSelectedElement) {
return;
}

View file

@ -10,7 +10,10 @@ import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DirectivePosition, SerializedInjectedService} from 'protocol';
import {DirectivePropertyResolver, DirectiveTreeData} from '../../property-resolver/directive-property-resolver';
import {
DirectivePropertyResolver,
DirectiveTreeData,
} from '../../property-resolver/directive-property-resolver';
import {FlatNode} from '../../property-resolver/element-property-resolver';
@Component({
@ -29,8 +32,11 @@ export class PropertyViewBodyComponent {
categoryOrder = [0, 1, 2];
get panels(): {
title: string; hidden: boolean; controls: DirectiveTreeData; documentation: string,
class: string;
title: string;
hidden: boolean;
controls: DirectiveTreeData;
documentation: string;
class: string;
}[] {
return [
{
@ -58,8 +64,11 @@ export class PropertyViewBodyComponent {
}
get controlsLoaded(): boolean {
return !!this.directiveStateControls && !!this.directiveOutputControls &&
!!this.directiveInputControls;
return (
!!this.directiveStateControls &&
!!this.directiveOutputControls &&
!!this.directiveInputControls
);
}
updateValue({node, newValue}: {node: FlatNode; newValue: unknown}): void {
@ -78,7 +87,6 @@ export class PropertyViewBodyComponent {
}
}
@Component({
selector: 'ng-dependency-viewer',
template: `
@ -87,23 +95,25 @@ export class PropertyViewBodyComponent {
<mat-expansion-panel-header collapsedHeight="35px" expandedHeight="35px">
<mat-panel-title>
<mat-chip-listbox>
<mat-chip matTooltipPosition="left" matTooltip="Dependency injection token" (click)="$event.stopPropagation();">{{dependency.token}}</mat-chip>
<mat-chip
matTooltipPosition="left"
matTooltip="Dependency injection token"
(click)="$event.stopPropagation()"
>{{ dependency.token }}</mat-chip
>
</mat-chip-listbox>
</mat-panel-title>
<mat-panel-description>
<mat-chip-listbox>
<div class="di-flags">
@if (dependency.flags?.optional) {
<mat-chip [highlighted]="true" color="primary">Optional</mat-chip>
}
@if (dependency.flags?.host) {
<mat-chip [highlighted]="true" color="primary">Host</mat-chip>
}
@if (dependency.flags?.self) {
<mat-chip [highlighted]="true" color="primary">Self</mat-chip>
}
@if (dependency.flags?.skipSelf) {
<mat-chip [highlighted]="true" color="primary">SkipSelf</mat-chip>
<mat-chip [highlighted]="true" color="primary">Optional</mat-chip>
} @if (dependency.flags?.host) {
<mat-chip [highlighted]="true" color="primary">Host</mat-chip>
} @if (dependency.flags?.self) {
<mat-chip [highlighted]="true" color="primary">Self</mat-chip>
} @if (dependency.flags?.skipSelf) {
<mat-chip [highlighted]="true" color="primary">SkipSelf</mat-chip>
}
</div>
</mat-chip-listbox>
@ -112,30 +122,32 @@ export class PropertyViewBodyComponent {
<ng-resolution-path [path]="dependency.resolutionPath"></ng-resolution-path>
</mat-expansion-panel>
</mat-accordion>
`,
styles: [`
.di-flags {
display: flex;
flex-wrap: nowrap;
}
:host-context(.dark-theme) ng-resolution-path {
background: #1a1a1a;
}
ng-resolution-path {
border-top: 1px solid black;
display: block;
overflow-x: scroll;
background: #f3f3f3;
}
:host {
mat-chip {
--mdc-chip-container-height: 18px;
`,
styles: [
`
.di-flags {
display: flex;
flex-wrap: nowrap;
}
}
`]
:host-context(.dark-theme) ng-resolution-path {
background: #1a1a1a;
}
ng-resolution-path {
border-top: 1px solid black;
display: block;
overflow-x: scroll;
background: #f3f3f3;
}
:host {
mat-chip {
--mdc-chip-container-height: 18px;
}
}
`,
],
})
export class DependencyViewerComponent {
@Input({required: true}) dependency!: SerializedInjectedService;
@ -143,16 +155,17 @@ export class DependencyViewerComponent {
@Component({
selector: 'ng-injected-services',
template: `
@for (dependency of dependencies; track dependency.position[0]) {
<ng-dependency-viewer [dependency]="dependency" />
}`,
styles: [`
template: ` @for (dependency of dependencies; track dependency.position[0]) {
<ng-dependency-viewer [dependency]="dependency" />
}`,
styles: [
`
ng-dependency-viewer {
border-bottom: 1px solid color-mix(in srgb, currentColor, #bdbdbd 85%);
display: block;
}
`]
`,
],
})
export class InjectedServicesComponent {
@Input({required: true}) controller!: DirectivePropertyResolver;

View file

@ -9,7 +9,10 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DirectivePosition} from 'protocol';
import {DirectivePropertyResolver, DirectiveTreeData} from '../../property-resolver/directive-property-resolver';
import {
DirectivePropertyResolver,
DirectiveTreeData,
} from '../../property-resolver/directive-property-resolver';
import {ElementPropertyResolver, FlatNode} from '../../property-resolver/element-property-resolver';
@Component({
@ -24,19 +27,19 @@ export class PropertyViewComponent {
constructor(private _nestedProps: ElementPropertyResolver) {}
get controller(): DirectivePropertyResolver|undefined {
get controller(): DirectivePropertyResolver | undefined {
return this._nestedProps.getDirectiveController(this.directive);
}
get directiveInputControls(): DirectiveTreeData|void {
get directiveInputControls(): DirectiveTreeData | void {
return this.controller?.directiveInputControls;
}
get directiveOutputControls(): DirectiveTreeData|void {
get directiveOutputControls(): DirectiveTreeData | void {
return this.controller?.directiveOutputControls;
}
get directiveStateControls(): DirectiveTreeData|void {
get directiveStateControls(): DirectiveTreeData | void {
return this.controller?.directiveStateControls;
}
}

View file

@ -23,7 +23,11 @@ import {ResolutionPathComponent} from '../../../dependency-injection/resolution-
import {PropertyEditorComponent} from './property-editor.component';
import {PropertyPreviewComponent} from './property-preview.component';
import {PropertyTabBodyComponent} from './property-tab-body.component';
import {DependencyViewerComponent, InjectedServicesComponent, PropertyViewBodyComponent} from './property-view-body.component';
import {
DependencyViewerComponent,
InjectedServicesComponent,
PropertyViewBodyComponent,
} from './property-view-body.component';
import {PropertyViewHeaderComponent} from './property-view-header.component';
import {PropertyViewTreeComponent} from './property-view-tree.component';
import {PropertyViewComponent} from './property-view.component';
@ -41,9 +45,17 @@ import {PropertyViewComponent} from './property-view.component';
DependencyViewerComponent,
],
imports: [
MatToolbarModule, MatButtonModule, MatIconModule, MatTreeModule, MatTooltipModule,
MatChipsModule, CommonModule, MatExpansionModule, DragDropModule, FormsModule,
ResolutionPathComponent
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatTreeModule,
MatTooltipModule,
MatChipsModule,
CommonModule,
MatExpansionModule,
DragDropModule,
FormsModule,
ResolutionPathComponent,
],
exports: [
PropertyViewComponent,
@ -55,5 +67,4 @@ import {PropertyViewComponent} from './property-view.component';
PropertyEditorComponent,
],
})
export class PropertyViewModule {
}
export class PropertyViewModule {}

View file

@ -18,126 +18,137 @@ import {Events, MessageBus, SerializedInjector, SerializedProviderRecord} from '
@Component({
selector: 'ng-injector-providers',
template: `
<h1>
Providers for {{ injector?.name }}
</h1>
@if (injector) {
<div class="injector-providers">
<mat-form-field appearance="fill" class="form-field-spacer">
<mat-label>Search by token</mat-label>
<input type="text" matInput
placeholder="Provider token"
(input)="searchToken.set($event.target.value)"
[value]="searchToken()"
/>
<mat-icon matSuffix (click)="searchToken.set('')">close</mat-icon>
</mat-form-field>
<mat-form-field class="form-field-spacer">
<mat-label>Search by type</mat-label>
<mat-select
[value]="searchType()"
(selectionChange)="searchType.set($event.value)"
>
<mat-option>None</mat-option>
@for (type of providerTypes; track type) {
<mat-option [value]="type">{{$any(providerTypeToLabel)[type]}}</mat-option>
}
</mat-select>
</mat-form-field>
@if (providers.length > 0) {
<table mat-table [dataSource]="providers" class="mat-elevation-z4">
<ng-container matColumnDef="token">
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Token</h3> </th>
<td mat-cell *matCellDef="let provider"> {{provider.token}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Type</h3> </th>
<td mat-cell *matCellDef="let provider">
@if (provider.type === 'multi') {
multi (x{{ provider.index.length }})
} @else {
{{$any(providerTypeToLabel)[provider.type]}}
}
</td>
</ng-container>
<ng-container matColumnDef="isViewProvider">
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title">Is View Provider</h3> </th>
<td mat-cell *matCellDef="let provider"> <mat-icon>{{provider.isViewProvider ? 'check_circle' : 'cancel'}}</mat-icon> </td>
</ng-container>
<ng-container matColumnDef="log">
<th mat-header-cell *matHeaderCellDef> <h3 class="column-title"></h3> </th>
<td mat-cell *matCellDef="let provider"> <mat-icon matTooltipPosition="left" matTooltip="Log provider in console" class="select" (click)="select(provider)">send</mat-icon> </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<h1>Providers for {{ injector?.name }}</h1>
@if (injector) {
<div class="injector-providers">
<mat-form-field appearance="fill" class="form-field-spacer">
<mat-label>Search by token</mat-label>
<input
type="text"
matInput
placeholder="Provider token"
(input)="searchToken.set($event.target.value)"
[value]="searchToken()"
/>
<mat-icon matSuffix (click)="searchToken.set('')">close</mat-icon>
</mat-form-field>
<mat-form-field class="form-field-spacer">
<mat-label>Search by type</mat-label>
<mat-select [value]="searchType()" (selectionChange)="searchType.set($event.value)">
<mat-option>None</mat-option>
@for (type of providerTypes; track type) {
<mat-option [value]="type">{{ $any(providerTypeToLabel)[type] }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (providers.length > 0) {
<table mat-table [dataSource]="providers" class="mat-elevation-z4">
<ng-container matColumnDef="token">
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Token</h3></th>
<td mat-cell *matCellDef="let provider">{{ provider.token }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Type</h3></th>
<td mat-cell *matCellDef="let provider">
@if (provider.type === 'multi') { multi (x{{ provider.index.length }}) } @else {
{{ $any(providerTypeToLabel)[provider.type] }}
}
</div>
}
`,
styles: [`
.select {
cursor: pointer;
}
</td>
</ng-container>
<ng-container matColumnDef="isViewProvider">
<th mat-header-cell *matHeaderCellDef><h3 class="column-title">Is View Provider</h3></th>
<td mat-cell *matCellDef="let provider">
<mat-icon>{{ provider.isViewProvider ? 'check_circle' : 'cancel' }}</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="log">
<th mat-header-cell *matHeaderCellDef><h3 class="column-title"></h3></th>
<td mat-cell *matCellDef="let provider">
<mat-icon
matTooltipPosition="left"
matTooltip="Log provider in console"
class="select"
(click)="select(provider)"
>send</mat-icon
>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
}
</div>
}
`,
styles: [
`
.select {
cursor: pointer;
}
:host {
display: block;
padding: 16px;
}
:host {
display: block;
padding: 16px;
}
.form-field-spacer {
margin: 0 4px 0 4px;
}
.form-field-spacer {
margin: 0 4px 0 4px;
}
table {
width: 100%;
}
table {
width: 100%;
}
.column-title {
margin: 0;
}
.column-title {
margin: 0;
}
tr.example-detail-row {
height: 0;
}
tr.example-detail-row {
height: 0;
}
.example-element-row td {
border-bottom-width: 0;
cursor: pointer;
}
.example-element-row td {
border-bottom-width: 0;
cursor: pointer;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
`],
.example-element-description-attribution {
opacity: 0.5;
}
`,
],
standalone: true,
imports: [
MatTableModule, MatIconModule, MatTooltipModule, MatInputModule, MatSelectModule,
MatFormFieldModule
MatTableModule,
MatIconModule,
MatTooltipModule,
MatInputModule,
MatSelectModule,
MatFormFieldModule,
],
})
export class InjectorProvidersComponent {
@ -152,11 +163,12 @@ export class InjectorProvidersComponent {
const predicates: ((provider: SerializedProviderRecord) => boolean)[] = [];
searchToken &&
predicates.push((provider) => provider.token.toLowerCase().includes(searchToken));
predicates.push((provider) => provider.token.toLowerCase().includes(searchToken));
searchType && predicates.push((provider) => provider.type === searchType);
return this.providers.filter(
(provider) => predicates.every((predicate) => predicate(provider)));
return this.providers.filter((provider) =>
predicates.every((predicate) => predicate(provider)),
);
});
providerTypeToLabel = {
@ -178,7 +190,7 @@ export class InjectorProvidersComponent {
type: this.injector.type,
name: this.injector.name,
},
row
row,
]);
}

View file

@ -8,7 +8,10 @@
import {DevToolsNode, SerializedInjector} from 'protocol';
import {InjectorTreeD3Node, InjectorTreeNode} from '../dependency-injection/injector-tree-visualizer';
import {
InjectorTreeD3Node,
InjectorTreeNode,
} from '../dependency-injection/injector-tree-visualizer';
export interface InjectorPath {
node: DevToolsNode;
@ -42,12 +45,15 @@ export function equalInjector(a: SerializedInjector, b: SerializedInjector): boo
}
export function findExistingPath(
path: InjectorTreeNode[], value: SerializedInjector): InjectorTreeNode|null {
return path.find(injector => equalInjector(injector.injector, value)) || null;
path: InjectorTreeNode[],
value: SerializedInjector,
): InjectorTreeNode | null {
return path.find((injector) => equalInjector(injector.injector, value)) || null;
}
export function transformInjectorResolutionPathsIntoTree(injectorPaths: InjectorPath[]):
InjectorTreeNode {
export function transformInjectorResolutionPathsIntoTree(
injectorPaths: InjectorPath[],
): InjectorTreeNode {
const injectorTree: InjectorTreeNode[] = [];
const injectorIdToNode = new Map<string, DevToolsNode>();
@ -83,8 +89,9 @@ export function transformInjectorResolutionPathsIntoTree(injectorPaths: Injector
return hiddenRoot as any;
}
export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNode[]):
InjectorPath[] {
export function grabInjectorPathsFromDirectiveForest(
directiveForest: DevToolsNode[],
): InjectorPath[] {
const injectorPaths: InjectorPath[] = [];
const grabInjectorPaths = (node: DevToolsNode) => {
@ -92,7 +99,7 @@ export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNo
injectorPaths.push({node, path: node.resolutionPath.slice().reverse()});
}
node.children.forEach(child => grabInjectorPaths(child));
node.children.forEach((child) => grabInjectorPaths(child));
};
for (const directive of directiveForest) {
@ -103,7 +110,8 @@ export function grabInjectorPathsFromDirectiveForest(directiveForest: DevToolsNo
}
export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths: InjectorPath[]): {
elementPaths: InjectorPath[]; environmentPaths: InjectorPath[];
elementPaths: InjectorPath[];
environmentPaths: InjectorPath[];
startingElementToEnvironmentPath: Map<string, SerializedInjector[]>;
} {
const elementPaths: InjectorPath[] = [];
@ -111,7 +119,7 @@ export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths:
const startingElementToEnvironmentPath = new Map<string, SerializedInjector[]>();
injectorPaths.forEach(({node, path}) => {
const firstElementIndex = path.findIndex(injector => injector.type === 'element');
const firstElementIndex = path.findIndex((injector) => injector.type === 'element');
// split the path into two paths,
// one for the element injector and one for the environment injector
@ -131,15 +139,18 @@ export function splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths:
if (elementPath[elementPath.length - 1]) {
// reverse each path to get the paths starting from the starting element
startingElementToEnvironmentPath.set(
elementPath[elementPath.length - 1].id, environmentPath.slice().reverse());
elementPath[elementPath.length - 1].id,
environmentPath.slice().reverse(),
);
}
});
return {
elementPaths:
elementPaths.filter(({path}) => path.every(injector => injector.type === 'element')),
elementPaths: elementPaths.filter(({path}) =>
path.every((injector) => injector.type === 'element'),
),
environmentPaths,
startingElementToEnvironmentPath
startingElementToEnvironmentPath,
};
}
@ -188,17 +199,20 @@ const ANGULAR_DIRECTIVES = [
'RouterLinkActive',
'RouterLinkWithHref',
'RouterOutlet',
'UpgradeComponent'
'UpgradeComponent',
];
const ignoredAngularInjectors = new Set([
'Null Injector', ...ANGULAR_DIRECTIVES, ...ANGULAR_DIRECTIVES.map(directive => `_${directive}`)
'Null Injector',
...ANGULAR_DIRECTIVES,
...ANGULAR_DIRECTIVES.map((directive) => `_${directive}`),
]);
export function filterOutInjectorsWithNoProviders(injectorPaths: InjectorPath[]): InjectorPath[] {
for (const injectorPath of injectorPaths) {
injectorPath.path =
injectorPath.path.filter(({providers}) => providers === undefined || providers > 0);
injectorPath.path = injectorPath.path.filter(
({providers}) => providers === undefined || providers > 0,
);
}
return injectorPaths;
@ -206,6 +220,6 @@ export function filterOutInjectorsWithNoProviders(injectorPaths: InjectorPath[])
export function filterOutAngularInjectors(injectorPaths: InjectorPath[]): InjectorPath[] {
return injectorPaths.map(({node, path}) => {
return {node, path: path.filter(injector => !ignoredAngularInjectors.has(injector.name))};
return {node, path: path.filter((injector) => !ignoredAngularInjectors.has(injector.name))};
});
}

View file

@ -14,25 +14,49 @@ import {MatExpansionModule} from '@angular/material/expansion';
import {MatIconModule} from '@angular/material/icon';
import {MatTabsModule} from '@angular/material/tabs';
import {MatTooltipModule} from '@angular/material/tooltip';
import {ComponentExplorerView, DevToolsNode, Events, MessageBus, SerializedInjector, SerializedProviderRecord} from 'protocol';
import {
ComponentExplorerView,
DevToolsNode,
Events,
MessageBus,
SerializedInjector,
SerializedProviderRecord,
} from 'protocol';
import {AngularSplitModule} from '../../vendor/angular-split/public_api';
import {InjectorTreeD3Node, InjectorTreeVisualizer} from '../dependency-injection/injector-tree-visualizer';
import {
InjectorTreeD3Node,
InjectorTreeVisualizer,
} from '../dependency-injection/injector-tree-visualizer';
import {ResolutionPathComponent} from '../dependency-injection/resolution-path.component';
import {InjectorProvidersComponent} from './injector-providers.component';
import {filterOutAngularInjectors, filterOutInjectorsWithNoProviders, generateEdgeIdsFromNodeIds, getInjectorIdsToRootFromNode, grabInjectorPathsFromDirectiveForest, splitInjectorPathsIntoElementAndEnvironmentPaths, transformInjectorResolutionPathsIntoTree} from './injector-tree-fns';
import {
filterOutAngularInjectors,
filterOutInjectorsWithNoProviders,
generateEdgeIdsFromNodeIds,
getInjectorIdsToRootFromNode,
grabInjectorPathsFromDirectiveForest,
splitInjectorPathsIntoElementAndEnvironmentPaths,
transformInjectorResolutionPathsIntoTree,
} from './injector-tree-fns';
@Component({
standalone: true,
selector: 'ng-injector-tree',
imports: [
MatButtonModule, AngularSplitModule, ResolutionPathComponent, MatTabsModule, MatExpansionModule,
InjectorProvidersComponent, MatIconModule, MatTooltipModule, MatCheckboxModule
MatButtonModule,
AngularSplitModule,
ResolutionPathComponent,
MatTabsModule,
MatExpansionModule,
InjectorProvidersComponent,
MatIconModule,
MatTooltipModule,
MatCheckboxModule,
],
templateUrl: `./injector-tree.component.html`,
styleUrls: ['./injector-tree.component.scss']
styleUrls: ['./injector-tree.component.scss'],
})
export class InjectorTreeComponent {
@ViewChild('svgContainer', {static: false}) private svgContainer!: ElementRef;
@ -45,7 +69,7 @@ export class InjectorTreeComponent {
zone = inject(NgZone);
firstRender = true;
selectedNode: InjectorTreeD3Node|null = null;
selectedNode: InjectorTreeD3Node | null = null;
rawDirectiveForest: DevToolsNode[] = [];
injectorTreeGraph!: InjectorTreeVisualizer;
elementInjectorTreeGraph!: InjectorTreeVisualizer;
@ -71,13 +95,13 @@ export class InjectorTreeComponent {
});
this._messageBus.on(
'latestInjectorProviders',
(_: SerializedInjector, providers: SerializedProviderRecord[]) => {
this.providers = Array.from(providers).sort((a, b) => {
return a.token.localeCompare(b.token);
});
'latestInjectorProviders',
(_: SerializedInjector, providers: SerializedProviderRecord[]) => {
this.providers = Array.from(providers).sort((a, b) => {
return a.token.localeCompare(b.token);
});
},
);
this._messageBus.on('highlightComponent', (id: number) => {
const injectorNode = this.getNodeByComponentId(this.elementInjectorTreeGraph, id);
@ -139,7 +163,7 @@ export class InjectorTreeComponent {
// In Angular we have two types of injectors, element injectors and environment injectors.
// We want to split the resolution paths into two groups, one for each type of injector.
const {elementPaths, environmentPaths, startingElementToEnvironmentPath} =
splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths);
splitInjectorPathsIntoElementAndEnvironmentPaths(injectorPaths);
this.elementToEnvironmentPath = startingElementToEnvironmentPath;
// Here for our 2 groups of resolution paths, we want to convert them into a tree structure.
@ -216,7 +240,7 @@ export class InjectorTreeComponent {
this.snapToRoot(this.elementInjectorTreeGraph);
}
getNodeByComponentId(graph: InjectorTreeVisualizer, id: number): InjectorTreeD3Node|null {
getNodeByComponentId(graph: InjectorTreeVisualizer, id: number): InjectorTreeD3Node | null {
const graphElement = graph.graphElement;
const element = graphElement.querySelector(`.node[data-component-id="${id}"]`);
if (element === null) {
@ -237,8 +261,10 @@ export class InjectorTreeComponent {
}
this.injectorTreeGraph?.cleanup?.();
this.injectorTreeGraph =
new InjectorTreeVisualizer(this.svgContainer.nativeElement, this.g.nativeElement);
this.injectorTreeGraph = new InjectorTreeVisualizer(
this.svgContainer.nativeElement,
this.g.nativeElement,
);
}
setUpElementInjectorVisualizer(): void {
@ -248,8 +274,10 @@ export class InjectorTreeComponent {
this.elementInjectorTreeGraph?.cleanup?.();
this.elementInjectorTreeGraph = new InjectorTreeVisualizer(
this.elementSvgContainer.nativeElement, this.elementG.nativeElement,
{nodeSeparation: () => 1});
this.elementSvgContainer.nativeElement,
this.elementG.nativeElement,
{nodeSeparation: () => 1},
);
}
highlightPathFromSelectedInjector(): void {
@ -266,21 +294,22 @@ export class InjectorTreeComponent {
if (this.selectedNode.data.injector.type === 'element') {
const idsToRoot = getInjectorIdsToRootFromNode(this.selectedNode);
idsToRoot.forEach(id => this.highlightNodeById(this.elementG, id));
idsToRoot.forEach((id) => this.highlightNodeById(this.elementG, id));
const edgeIds = generateEdgeIdsFromNodeIds(idsToRoot);
edgeIds.forEach((edgeId => this.highlightEdgeById(this.elementG, edgeId)));
edgeIds.forEach((edgeId) => this.highlightEdgeById(this.elementG, edgeId));
const environmentPath =
this.elementToEnvironmentPath.get(this.selectedNode.data.injector.id) ?? [];
environmentPath.forEach(injector => this.highlightNodeById(this.g, injector.id));
const environmentEdgeIds =
generateEdgeIdsFromNodeIds(environmentPath.map(injector => injector.id));
environmentEdgeIds.forEach(edgeId => this.highlightEdgeById(this.g, edgeId));
this.elementToEnvironmentPath.get(this.selectedNode.data.injector.id) ?? [];
environmentPath.forEach((injector) => this.highlightNodeById(this.g, injector.id));
const environmentEdgeIds = generateEdgeIdsFromNodeIds(
environmentPath.map((injector) => injector.id),
);
environmentEdgeIds.forEach((edgeId) => this.highlightEdgeById(this.g, edgeId));
} else {
const idsToRoot = getInjectorIdsToRootFromNode(this.selectedNode);
idsToRoot.forEach(id => this.highlightNodeById(this.g, id));
idsToRoot.forEach((id) => this.highlightNodeById(this.g, id));
const edgeIds = generateEdgeIdsFromNodeIds(idsToRoot);
edgeIds.forEach(edgeId => this.highlightEdgeById(this.g, edgeId));
edgeIds.forEach((edgeId) => this.highlightEdgeById(this.g, edgeId));
}
}
@ -331,10 +360,12 @@ export class InjectorTreeComponent {
return;
}
const injector = this.selectedNode.data.injector;
this._messageBus.emit('getInjectorProviders', [{
id: injector.id,
type: injector.type,
name: injector.name,
}]);
this._messageBus.emit('getInjectorProviders', [
{
id: injector.id,
type: injector.type,
name: injector.name,
},
]);
}
}

View file

@ -33,8 +33,9 @@ export class FileApiService {
saveObjectAsJSON(object: object): void {
const downloadLink = document.createElement('a');
downloadLink.download = `NgDevTools-Profile-${toISO8601Compact(new Date())}.json`;
downloadLink.href =
URL.createObjectURL(new Blob([JSON.stringify(object)], {type: 'application/json'}));
downloadLink.href = URL.createObjectURL(
new Blob([JSON.stringify(object)], {type: 'application/json'}),
);
downloadLink.click();
setTimeout(() => URL.revokeObjectURL(downloadLink.href));
}

View file

@ -13,7 +13,7 @@ interface DialogData {
profilerVersion?: number;
importedVersion?: number;
errorMessage?: string;
status: 'ERROR'|'INVALID_VERSION';
status: 'ERROR' | 'INVALID_VERSION';
}
@Component({
@ -23,6 +23,7 @@ interface DialogData {
})
export class ProfilerImportDialogComponent {
constructor(
public dialogRef: MatDialogRef<ProfilerImportDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) {}
public dialogRef: MatDialogRef<ProfilerImportDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
) {}
}

View file

@ -14,7 +14,7 @@ import {Subject, Subscription} from 'rxjs';
import {FileApiService} from './file-api-service';
import {ProfilerImportDialogComponent} from './profiler-import-dialog.component';
type State = 'idle'|'recording'|'visualizing';
type State = 'idle' | 'recording' | 'visualizing';
const SUPPORTED_VERSIONS = [1];
const PROFILER_VERSION = 1;
@ -32,9 +32,9 @@ export class ProfilerComponent implements OnInit {
private _buffer: ProfilerFrame[] = [];
constructor(
private _fileApiService: FileApiService,
private _messageBus: MessageBus<Events>,
public dialog: MatDialog,
private _fileApiService: FileApiService,
private _messageBus: MessageBus<Events>,
public dialog: MatDialog,
) {
this._fileApiService.uploadedData.subscribe((importedFile) => {
if (importedFile.error) {

View file

@ -35,5 +35,4 @@ import {TimelineModule} from './timeline/timeline.module';
],
exports: [ProfilerComponent],
})
export class ProfilerModule {
}
export class ProfilerModule {}

View file

@ -85,163 +85,191 @@ describe('filtering', () => {
describe('filtering', () => {
it('should filter results with a source query', () => {
const filter = createFilter('source:click');
expect(filter({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeTrue();
expect(filter({
frame: {
directives: [],
duration: 10,
source: 'mouseenter',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 10,
source: 'mouseenter',
},
style: {},
toolTip: '',
}),
).toBeFalse();
});
it('should filter results with a duration query', () => {
const filter1 = createFilter('duration:>10ms');
expect(filter1({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter1({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(filter1({
frame: {
directives: [],
duration: 15,
source: 'mouseenter',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter1({
frame: {
directives: [],
duration: 15,
source: 'mouseenter',
},
style: {},
toolTip: '',
}),
).toBeTrue();
const filter2 = createFilter('duration:=10ms');
expect(filter2({
frame: {
directives: [],
duration: 11,
source: 'mouseenter',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(filter2({
frame: {
directives: [],
duration: 10,
source: 'mouseenter',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter2({
frame: {
directives: [],
duration: 11,
source: 'mouseenter',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(
filter2({
frame: {
directives: [],
duration: 10,
source: 'mouseenter',
},
style: {},
toolTip: '',
}),
).toBeTrue();
});
it('should work with composite selectors', () => {
const filter = createFilter('duration:>10ms source: click');
expect(filter({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 10,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(filter({
frame: {
directives: [],
duration: 15,
source: 'mouseenter',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 15,
source: 'mouseenter',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeTrue();
});
it('should work with invalid arguments', () => {
const filter = createFilter('duration:>ms');
expect(filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeTrue();
});
it('should work with negation', () => {
const filter = createFilter('!source:message');
expect(filter({
frame: {
directives: [],
duration: 15,
source: 'message',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 15,
source: 'message',
},
style: {},
toolTip: '',
}),
).toBeFalse();
});
it('should work with negation and composite expressions', () => {
const filter = createFilter('!duration:=15 !source:message source:click');
expect(filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 15,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(filter({
frame: {
directives: [],
duration: 10,
source: 'message',
},
style: {},
toolTip: '',
})).toBeFalse();
expect(
filter({
frame: {
directives: [],
duration: 10,
source: 'message',
},
style: {},
toolTip: '',
}),
).toBeFalse();
expect(filter({
frame: {
directives: [],
duration: 14,
source: 'click',
},
style: {},
toolTip: '',
})).toBeTrue();
expect(
filter({
frame: {
directives: [],
duration: 14,
source: 'click',
},
style: {},
toolTip: '',
}),
).toBeTrue();
});
});
});

View file

@ -16,11 +16,11 @@ export const noopFilter = (_: GraphNode) => true;
interface Query<Arguments = unknown> {
readonly name: QueryType;
parseArguments(args: string[]): Arguments|undefined;
parseArguments(args: string[]): Arguments | undefined;
apply(node: ProfilerFrame, args: Arguments): boolean;
}
type Operator = '>'|'<'|'='|'<='|'>=';
type Operator = '>' | '<' | '=' | '<=' | '>=';
const ops: {[operator in Operator]: (a: number, b: number) => boolean} = {
'>'(a: number, b: number): boolean {
@ -48,12 +48,12 @@ const enum QueryType {
Source = 'source',
}
type QueryArguments = DurationArgument|SourceArgument;
type QueryArguments = DurationArgument | SourceArgument;
const operatorRe = /^(>=|<=|=|<|>|)/;
class DurationQuery implements Query<DurationArgument> {
readonly name = QueryType.Duration;
parseArguments([arg]: string[]): DurationArgument|undefined {
parseArguments([arg]: string[]): DurationArgument | undefined {
arg = arg.trim();
const operator = (arg.match(operatorRe) ?? [null])[0];
if (!operator) {
@ -82,15 +82,17 @@ class SourceQuery implements Query<SourceArgument> {
}
}
const queryMap: {[query in QueryType]: Query} =
[new DurationQuery(), new SourceQuery()].reduce((map, query) => {
map[query.name] = query;
return map;
}, {} as {[query in QueryType]: Query});
const queryMap: {[query in QueryType]: Query} = [new DurationQuery(), new SourceQuery()].reduce(
(map, query) => {
map[query.name] = query;
return map;
},
{} as {[query in QueryType]: Query},
);
const queryRe = new RegExp(`!?s*(${QueryType.Duration}|${QueryType.Source})$`, 'g');
type Predicate = true|false;
type Predicate = true | false;
type QueryAST = [Predicate, QueryType, QueryArguments];
/**

View file

@ -7,7 +7,15 @@
*/
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Component, DestroyRef, ElementRef, EventEmitter, Input, Output, ViewChild,} from '@angular/core';
import {
Component,
DestroyRef,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Observable, Subscription} from 'rxjs';
@ -28,11 +36,14 @@ export class FrameSelectorComponent {
set graphData$(graphData: Observable<GraphNode[]>) {
this._graphData$ = graphData;
this._graphDataSubscription?.unsubscribe();
this._graphDataSubscription = this._graphData$.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((items) => setTimeout(() => {
this.frameCount = items.length;
this.viewport.scrollToIndex(items.length);
}));
this._graphDataSubscription = this._graphData$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((items) =>
setTimeout(() => {
this.frameCount = items.length;
this.viewport.scrollToIndex(items.length);
}),
);
}
get graphData$(): Observable<GraphNode[]> {
@ -57,10 +68,9 @@ export class FrameSelectorComponent {
private _graphData$!: Observable<GraphNode[]>;
private _graphDataSubscription?: Subscription;
constructor(
private _tabUpdate: TabUpdate,
private destroyRef: DestroyRef,
private _tabUpdate: TabUpdate,
private destroyRef: DestroyRef,
) {
this._tabUpdate.tabUpdate$.pipe(takeUntilDestroyed()).subscribe(() => {
if (this.viewport) {
@ -84,7 +94,7 @@ export class FrameSelectorComponent {
const sortedIndexes = indexArray.sort((a, b) => a - b);
const groups: number[][] = [];
let prev: number|null = null;
let prev: number | null = null;
for (const index of sortedIndexes) {
// First iteration: create initial group and set prev variable to the first index
@ -104,12 +114,12 @@ export class FrameSelectorComponent {
prev = index;
}
return groups.filter((group) => group.length > 0)
.map(
(group) => group.length === 1 ? group[0] + 1 :
`${group[0] + 1}-${group[group.length - 1] + 1}`,
)
.join(', ');
return groups
.filter((group) => group.length > 0)
.map((group) =>
group.length === 1 ? group[0] + 1 : `${group[0] + 1}-${group[group.length - 1] + 1}`,
)
.join(', ');
}
move(value: number): void {
@ -134,12 +144,12 @@ export class FrameSelectorComponent {
if (shiftKey) {
const [start, end] = [Math.min(this.startFrameIndex, idx), Math.max(this.endFrameIndex, idx)];
this.selectedFrameIndexes = new Set(
Array.from(Array(end - start + 1), (_, index) => index + start),
Array.from(Array(end - start + 1), (_, index) => index + start),
);
} else if (ctrlKey || metaKey) {
if (this.selectedFrameIndexes.has(idx)) {
if (this.selectedFrameIndexes.size === 1) {
return; // prevent deselection when only one frame is selected
return; // prevent deselection when only one frame is selected
}
this.selectedFrameIndexes.delete(idx);
@ -190,6 +200,6 @@ export class FrameSelectorComponent {
const dragScrollSpeed = 2;
const dx = event.clientX - this._viewportScrollState.xCoordinate;
this.viewport.elementRef.nativeElement.scrollLeft =
this._viewportScrollState.scrollLeft - dx * dragScrollSpeed;
this._viewportScrollState.scrollLeft - dx * dragScrollSpeed;
}
}

View file

@ -16,7 +16,7 @@ export interface BargraphNode {
value: number;
label: string;
original: ElementProfile;
count: number; // number of merged nodes with the same label
count: number; // number of merged nodes with the same label
directives?: DirectiveProfile[];
}
@ -55,7 +55,10 @@ export class BarGraphFormatter extends RecordFormatter<BargraphNode[]> {
}
override addFrame(
nodes: BargraphNode[], elements: ElementProfile[], parents: ElementProfile[] = []): number {
nodes: BargraphNode[],
elements: ElementProfile[],
parents: ElementProfile[] = [],
): number {
let timeSpent = 0;
elements.forEach((element) => {
// Possibly undefined because of the insertion on the backend.

View file

@ -7,7 +7,12 @@
*/
import {AppEntry} from '../record-formatter';
import {NESTED_FORMATTED_FLAMEGRAPH_RECORD, NESTED_RECORD, SIMPLE_FORMATTED_FLAMEGRAPH_RECORD, SIMPLE_RECORD,} from '../record-formatter-spec-constants';
import {
NESTED_FORMATTED_FLAMEGRAPH_RECORD,
NESTED_RECORD,
SIMPLE_FORMATTED_FLAMEGRAPH_RECORD,
SIMPLE_RECORD,
} from '../record-formatter-spec-constants';
import {FlamegraphFormatter, FlamegraphNode} from './flamegraph-formatter';

View file

@ -24,8 +24,11 @@ export interface FlamegraphNode {
export const ROOT_LEVEL_ELEMENT_LABEL = 'Entire application';
export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
override formatFrame(frame: ProfilerFrame, showChangeDetection?: boolean, theme?: Theme):
FlamegraphNode {
override formatFrame(
frame: ProfilerFrame,
showChangeDetection?: boolean,
theme?: Theme,
): FlamegraphNode {
const result: FlamegraphNode = {
value: 0,
label: ROOT_LEVEL_ELEMENT_LABEL,
@ -40,7 +43,7 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
if (showChangeDetection) {
result.color =
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
}
this.addFrame(result.children, frame.directives, showChangeDetection, theme);
@ -48,8 +51,11 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
}
override addFrame(
nodes: FlamegraphNode[], elements: ElementProfile[], showChangeDetection?: boolean,
theme?: Theme): number {
nodes: FlamegraphNode[],
elements: ElementProfile[],
showChangeDetection?: boolean,
theme?: Theme,
): number {
let timeSpent = 0;
elements.forEach((element) => {
// Possibly undefined because of
@ -69,7 +75,7 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
};
if (showChangeDetection) {
const CHANGE_DETECTION_COLOR =
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
theme === 'dark-theme' ? CHANGE_DETECTION_COLOR_DARK : CHANGE_DETECTION_COLOR_LIGHT;
node.color = changeDetected ? CHANGE_DETECTION_COLOR : NO_CHANGE_DETECTION_COLOR;
}
timeSpent += this.addFrame(node.children, element.children, showChangeDetection, theme);

View file

@ -8,7 +8,7 @@
import {DirectiveProfile, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
const mergeProperty = (mergeInProp: number|undefined, value: number|undefined) => {
const mergeProperty = (mergeInProp: number | undefined, value: number | undefined) => {
if (mergeInProp === undefined) {
return value;
}
@ -52,7 +52,7 @@ const mergeFrame = (mergeIn: ProfilerFrame, second: ProfilerFrame) => {
mergeDirectives(mergeIn.directives, second.directives);
};
export const mergeFrames = (frames: ProfilerFrame[]): ProfilerFrame|null => {
export const mergeFrames = (frames: ProfilerFrame[]): ProfilerFrame | null => {
if (!frames || !frames.length) {
return null;
}

View file

@ -334,15 +334,10 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
changeDetected: true,
children: [],
instances: 1,
original: NESTED_RECORD[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0],
original:
NESTED_RECORD[0].children[0].children[0].children[0]
.children[0].children[0].children[0].children[0]
.children[0],
},
{
value: 0,
@ -350,15 +345,10 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
changeDetected: true,
children: [],
instances: 1,
original: NESTED_RECORD[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[1],
original:
NESTED_RECORD[0].children[0].children[0].children[0]
.children[0].children[0].children[0].children[0]
.children[1],
},
{
value: 0,
@ -366,38 +356,28 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
changeDetected: true,
children: [],
instances: 1,
original: NESTED_RECORD[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[2],
original:
NESTED_RECORD[0].children[0].children[0].children[0]
.children[0].children[0].children[0].children[0]
.children[2],
},
],
instances: 1,
original: NESTED_RECORD[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0]
.children[0],
original:
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
.children[0].children[0].children[0],
},
],
instances: 1,
original:
NESTED_RECORD[0].children[0].children[0].children[0].children
[0].children
[0].children[0],
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
.children[0].children[0],
},
],
instances: 1,
original: NESTED_RECORD[0].children[0].children[0].children[0].children
[0].children[0],
original:
NESTED_RECORD[0].children[0].children[0].children[0].children[0]
.children[0],
},
],
instances: 1,

View file

@ -28,13 +28,15 @@ describe('getValue cases', () => {
it('calculates value with no lifecycle hooks', () => {
element = {
children: [],
directives: [{
changeDetection: 10,
isElement: false,
isComponent: true,
lifecycle: {},
name: 'AppComponent'
}],
directives: [
{
changeDetection: 10,
isElement: false,
isComponent: true,
lifecycle: {},
name: 'AppComponent',
},
],
};
expect(formatter.getValue(element)).toBe(10);
});
@ -48,7 +50,7 @@ describe('getValue cases', () => {
isElement: false,
name: 'NgForOf',
lifecycle: {ngDoCheck: 5},
changeDetection: 0
changeDetection: 0,
},
],
};
@ -64,7 +66,7 @@ describe('getValue cases', () => {
isElement: false,
name: 'NgForOf',
lifecycle: {ngDoCheck: 5},
changeDetection: 10
changeDetection: 10,
},
],
};
@ -143,7 +145,7 @@ describe('getLabel cases', () => {
isComponent: true,
lifecycle: {},
outputs: {},
name: 'TodoComponent'
name: 'TodoComponent',
},
],
};
@ -176,7 +178,7 @@ describe('getLabel cases', () => {
isComponent: true,
lifecycle: {},
outputs: {},
name: 'TodoComponent'
name: 'TodoComponent',
},
],
};

View file

@ -26,13 +26,16 @@ export interface GraphNode {
export abstract class RecordFormatter<T> {
abstract formatFrame(frame: ProfilerFrame): T;
abstract addFrame(nodes: T|T[], elements: ElementProfile[]): number|void;
abstract addFrame(nodes: T | T[], elements: ElementProfile[]): number | void;
getLabel(element: ElementProfile): string {
const name = element.directives.filter((d) => d.isComponent).map((c) => c.name).join(', ');
const attributes =
[...new Set(element.directives.filter((d) => !d.isComponent).map((d) => d.name))].join(
', ');
const name = element.directives
.filter((d) => d.isComponent)
.map((c) => c.name)
.join(', ');
const attributes = [
...new Set(element.directives.filter((d) => !d.isComponent).map((d) => d.name)),
].join(', ');
return attributes === '' ? name : `${name}[${attributes}]`;
}

View file

@ -16,7 +16,7 @@ export interface TreeMapNode {
value: number;
size: number;
children: TreeMapNode[];
original: ElementProfile|null;
original: ElementProfile | null;
}
export class TreeMapFormatter extends RecordFormatter<TreeMapNode> {
@ -37,7 +37,10 @@ export class TreeMapFormatter extends RecordFormatter<TreeMapNode> {
}
override addFrame(
nodes: TreeMapNode[], elements: ElementProfile[], prev: TreeMapNode|null = null): void {
nodes: TreeMapNode[],
elements: ElementProfile[],
prev: TreeMapNode | null = null,
): void {
elements.forEach((element) => {
if (!element) {
console.error('Unable to insert undefined element');

Some files were not shown because too many files have changed in this diff Show more