Skip to content

Commit d639a8d

Browse files
dgp1130thePunderWoman
authored andcommitted
refactor(devtools): display directive metadata from Wiz and ACX (angular#60475)
This updates the DevTools protocol to send Wiz/ACX metadata in addition to Angular metadata. Fortunately we don't need to worry about backwards compatibility here (`framework` is required for example), but the design roughly mirrors `DirectiveDebugMetadata` in `@angular/core`. Beyond that, this is mostly plumbing through an extra data slice in the form of `props` provided by Wiz. An earlier version implemented `events` as their own slice as well, but was removed as there is currently no generic way to disambiguate events from any other form of callback passed in as a prop. Instead, event callbacks are visualized as functions under the "Props" category. Working with `DirectiveMetadata` as a union is unfortunately a bit annoying since it requires casting to more specific `{Angular,Acx,Wiz}DirectiveMetadata` types for TS to allow property access, even when the properties are optional anyways. This commit is mostly for adding Wiz, but does add a bit of ACX functionality which is not fully tested. PR Close angular#60475
1 parent ebf6516 commit d639a8d

File tree

10 files changed

+244
-61
lines changed

10 files changed

+244
-61
lines changed

devtools/projects/ng-devtools-backend/src/lib/component-tree.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
Type,
1717
ValueProvider,
1818
ɵAngularComponentDebugMetadata as AngularComponentDebugMetadata,
19-
ɵAngularDirectiveDebugMetadata as AngularDirectiveDebugMetadata,
19+
ɵAcxComponentDebugMetadata as AcxComponentDebugMetadata,
2020
ɵProviderRecord as ProviderRecord,
2121
} from '@angular/core';
2222
import {
@@ -52,6 +52,17 @@ enum ChangeDetectionStrategy {
5252
Default = 1,
5353
}
5454

55+
enum AcxChangeDetectionStrategy {
56+
Default = 0,
57+
OnPush = 1,
58+
}
59+
60+
enum Framework {
61+
Angular = 'angular',
62+
ACX = 'acx',
63+
Wiz = 'wiz',
64+
}
65+
5566
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
5667
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
5768
export const idToInjector = new Map<string, Injector>();
@@ -121,7 +132,7 @@ export const getLatestComponentState = (
121132
const populateResultSet = (dir: DirectiveInstanceType | ComponentInstanceType) => {
122133
const {instance, name} = dir;
123134
const metadata = getDirectiveMetadata(instance);
124-
if (injector) {
135+
if (injector && metadata.framework === Framework.Angular) {
125136
metadata.dependencies = getDependenciesForDirective(
126137
injector,
127138
resolutionPathWithProviders,
@@ -220,16 +231,44 @@ const enum DirectiveMetadataKey {
220231
// the method directly interacts with the directive/component definition.
221232
const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
222233
const getMetadata = ngDebugClient().getDirectiveMetadata!;
223-
const metadata = getMetadata?.(dir) as
224-
| (AngularDirectiveDebugMetadata & Partial<AngularComponentDebugMetadata>)
225-
| null;
234+
const metadata = getMetadata?.(dir);
226235
if (metadata) {
227-
return {
228-
inputs: metadata.inputs,
229-
outputs: metadata.outputs,
230-
encapsulation: metadata.encapsulation,
231-
onPush: metadata.changeDetection === ChangeDetectionStrategy.OnPush,
232-
};
236+
const {framework} = metadata;
237+
switch (framework) {
238+
case undefined: // Back compat, older Angular versions did not set `framework`.
239+
case Framework.Angular: {
240+
const meta = metadata as typeof metadata & Partial<AngularComponentDebugMetadata>;
241+
return {
242+
framework: Framework.Angular,
243+
name: meta.name,
244+
inputs: meta.inputs,
245+
outputs: meta.outputs,
246+
encapsulation: meta.encapsulation,
247+
onPush: meta.changeDetection === ChangeDetectionStrategy.OnPush,
248+
};
249+
}
250+
case Framework.ACX: {
251+
const meta = metadata as typeof metadata & Partial<AcxComponentDebugMetadata>;
252+
return {
253+
framework: Framework.ACX,
254+
name: meta.name,
255+
inputs: meta.inputs,
256+
outputs: meta.outputs,
257+
encapsulation: meta.encapsulation,
258+
onPush: meta.changeDetection === AcxChangeDetectionStrategy.OnPush,
259+
};
260+
}
261+
case Framework.Wiz: {
262+
return {
263+
framework: Framework.Wiz,
264+
name: metadata.name,
265+
props: metadata.props,
266+
};
267+
}
268+
default: {
269+
throw new Error(`Unknown framework: "${framework}".`);
270+
}
271+
}
233272
}
234273

235274
// Used in older Angular versions, prior to the introduction of `getDirectiveMetadata`.
@@ -243,6 +282,7 @@ const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
243282
};
244283

245284
return {
285+
framework: Framework.Angular,
246286
inputs: safelyGrabMetadata(DirectiveMetadataKey.INPUTS),
247287
outputs: safelyGrabMetadata(DirectiveMetadataKey.OUTPUTS),
248288
encapsulation: safelyGrabMetadata(DirectiveMetadataKey.ENCAPSULATION),
@@ -252,7 +292,16 @@ const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
252292

253293
export function isOnPushDirective(dir: any): boolean {
254294
const metadata = getDirectiveMetadata(dir.instance);
255-
return Boolean(metadata.onPush);
295+
switch (metadata.framework) {
296+
case Framework.Angular:
297+
return Boolean(metadata.onPush);
298+
case Framework.ACX:
299+
return Boolean(metadata.onPush);
300+
case Framework.Wiz:
301+
return false;
302+
default:
303+
throw new Error(`Unknown framework: "${metadata.framework}".`);
304+
}
256305
}
257306

258307
export function getInjectorProviders(injector: Injector) {

devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
12
load("//devtools/tools:ng_module.bzl", "ng_module")
23
load("//devtools/tools:typescript.bzl", "ts_test_library")
3-
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
44

55
package(default_visibility = ["//visibility:public"])
66

@@ -38,6 +38,7 @@ ts_test_library(
3838
":property-resolver",
3939
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
4040
"//devtools/projects/protocol",
41+
"//packages/core",
4142
"@npm//@angular/cdk",
4243
"@npm//@angular/material",
4344
],

devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/directive-property-resolver.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {ɵFramework as Framework} from '@angular/core';
910
import {Properties, PropType} from 'protocol';
1011

1112
import {DirectivePropertyResolver} from './directive-property-resolver';
@@ -98,6 +99,7 @@ const properties: Properties = {
9899
},
99100
},
100101
metadata: {
102+
framework: Framework.Angular,
101103
inputs: {
102104
i: 'i',
103105
i1: 'i_1',
@@ -133,6 +135,36 @@ describe('DirectivePropertyResolver', () => {
133135
expect(resolver.directiveStateControls.dataSource.data[0].prop.name).toBe('p');
134136
});
135137

138+
it('should register directive props', () => {
139+
const resolver = new DirectivePropertyResolver(
140+
messageBusMock,
141+
{
142+
props: {
143+
foo: {
144+
editable: false,
145+
expandable: false,
146+
preview: '',
147+
type: PropType.Object,
148+
value: {},
149+
containerType: null,
150+
},
151+
},
152+
metadata: {
153+
framework: Framework.Wiz,
154+
props: {
155+
foo: 'foo',
156+
},
157+
},
158+
},
159+
{
160+
element: [0],
161+
directive: 0,
162+
},
163+
);
164+
165+
expect(resolver.directivePropControls.dataSource.data[0].prop.name).toBe('foo');
166+
});
167+
136168
it('should sort properties', () => {
137169
const resolver = new DirectivePropertyResolver(messageBusMock, properties, {
138170
element: [0],

devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/directive-property-resolver.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {FlatTreeControl} from '@angular/cdk/tree';
10-
import {ViewEncapsulation} from '@angular/core';
1110
import {
1211
Descriptor,
1312
DirectiveMetadata,
@@ -59,6 +58,7 @@ export class DirectivePropertyResolver {
5958
);
6059

6160
private _inputsDataSource: PropertyDataSource;
61+
private _propsDataSource: PropertyDataSource;
6262
private _outputsDataSource: PropertyDataSource;
6363
private _stateDataSource: PropertyDataSource;
6464

@@ -67,9 +67,10 @@ export class DirectivePropertyResolver {
6767
private _props: Properties,
6868
private _directivePosition: DirectivePosition,
6969
) {
70-
const {inputs, outputs, state} = this._classifyProperties();
70+
const {inputs, props, outputs, state} = this._classifyProperties();
7171

7272
this._inputsDataSource = this._createDataSourceFromProps(inputs);
73+
this._propsDataSource = this._createDataSourceFromProps(props);
7374
this._outputsDataSource = this._createDataSourceFromProps(outputs);
7475
this._stateDataSource = this._createDataSourceFromProps(state);
7576
}
@@ -78,6 +79,10 @@ export class DirectivePropertyResolver {
7879
return getDirectiveControls(this._inputsDataSource);
7980
}
8081

82+
get directivePropControls(): DirectiveTreeData {
83+
return getDirectiveControls(this._propsDataSource);
84+
}
85+
8186
get directiveOutputControls(): DirectiveTreeData {
8287
return getDirectiveControls(this._outputsDataSource);
8388
}
@@ -98,27 +103,21 @@ export class DirectivePropertyResolver {
98103
return this._directivePosition;
99104
}
100105

101-
get directiveViewEncapsulation(): ViewEncapsulation | undefined {
102-
return this._props.metadata?.encapsulation;
103-
}
104-
105-
get directiveHasOnPushStrategy(): boolean | undefined {
106-
return this._props.metadata?.onPush;
107-
}
108-
109106
getExpandedProperties(): NestedProp[] {
110107
return [
111108
...getExpandedDirectiveProperties(this._inputsDataSource.data),
109+
...getExpandedDirectiveProperties(this._propsDataSource.data),
112110
...getExpandedDirectiveProperties(this._outputsDataSource.data),
113111
...getExpandedDirectiveProperties(this._stateDataSource.data),
114112
];
115113
}
116114

117115
updateProperties(newProps: Properties): void {
118116
this._props = newProps;
119-
const {inputs, outputs, state} = this._classifyProperties();
117+
const {inputs, props, outputs, state} = this._classifyProperties();
120118

121119
this._inputsDataSource.update(inputs);
120+
this._propsDataSource.update(props);
122121
this._outputsDataSource.update(outputs);
123122
this._stateDataSource.update(state);
124123
}
@@ -142,28 +141,44 @@ export class DirectivePropertyResolver {
142141

143142
private _classifyProperties(): {
144143
inputs: {[name: string]: Descriptor};
144+
props: {[name: string]: Descriptor};
145145
outputs: {[name: string]: Descriptor};
146146
state: {[name: string]: Descriptor};
147147
} {
148-
const inputLabels: Set<string> = new Set(Object.values(this._props.metadata?.inputs || {}));
149-
const outputLabels: Set<string> = new Set(Object.values(this._props.metadata?.outputs || {}));
150-
151-
const inputs = {};
152-
const outputs = {};
153-
const state = {};
154-
let propPointer: {[name: string]: Descriptor};
155-
156-
Object.keys(this.directiveProperties).forEach((propName) => {
157-
propPointer = inputLabels.has(propName)
158-
? inputs
159-
: outputLabels.has(propName)
160-
? outputs
161-
: state;
162-
propPointer[propName] = this.directiveProperties[propName];
163-
});
148+
const metadata = this._props.metadata;
149+
if (!metadata) {
150+
return {
151+
inputs: {},
152+
props: {},
153+
outputs: {},
154+
state: this.directiveProperties,
155+
};
156+
}
157+
158+
const inputLabels = new Set('inputs' in metadata ? Object.values(metadata.inputs) : []);
159+
const propLabels = new Set('props' in metadata ? Object.values(metadata.props) : []);
160+
const outputLabels = new Set('outputs' in metadata ? Object.values(metadata.outputs) : []);
161+
162+
const inputs: {[name: string]: Descriptor} = {};
163+
const props: {[name: string]: Descriptor} = {};
164+
const outputs: {[name: string]: Descriptor} = {};
165+
const state: {[name: string]: Descriptor} = {};
166+
167+
for (const [propName, value] of Object.entries(this.directiveProperties)) {
168+
if (inputLabels.has(propName)) {
169+
inputs[propName] = value;
170+
} else if (propLabels.has(propName)) {
171+
props[propName] = value;
172+
} else if (outputLabels.has(propName)) {
173+
outputs[propName] = value;
174+
} else {
175+
state[propName] = value;
176+
}
177+
}
164178

165179
return {
166180
inputs,
181+
props,
167182
outputs,
168183
state,
169184
};

devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/component-metadata.component.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ChangeDetectionStrategy, Component, computed, inject, input} from '@angular/core';
10-
import {ComponentType} from 'protocol';
9+
import {
10+
ChangeDetectionStrategy,
11+
Component,
12+
ɵFramework as Framework,
13+
computed,
14+
inject,
15+
input,
16+
} from '@angular/core';
17+
import {AngularDirectiveMetadata, AcxDirectiveMetadata, ComponentType} from 'protocol';
1118

1219
import {ElementPropertyResolver} from '../property-resolver/element-property-resolver';
1320

@@ -22,7 +29,8 @@ export class ComponentMetadataComponent {
2229

2330
private _nestedProps = inject(ElementPropertyResolver);
2431

25-
viewEncapsulationModes = ['Emulated', 'Native', 'None', 'ShadowDom'];
32+
angularViewEncapsulationModes = ['Emulated', 'Native', 'None', 'ShadowDom'];
33+
acxViewEncapsulationModes = ['Emulated', 'None'];
2634

2735
readonly controller = computed(() => {
2836
const comp = this.currentSelectedComponent();
@@ -33,15 +41,32 @@ export class ComponentMetadataComponent {
3341
});
3442

3543
readonly viewEncapsulation = computed(() => {
36-
const encapsulationIndex = this.controller()?.directiveViewEncapsulation;
37-
if (encapsulationIndex !== undefined) {
38-
return this.viewEncapsulationModes[encapsulationIndex];
44+
const metadata = this.controller()?.directiveMetadata;
45+
if (!metadata) return undefined;
46+
47+
const encapsulation = (metadata as AngularDirectiveMetadata | AcxDirectiveMetadata)
48+
.encapsulation;
49+
if (!encapsulation) return undefined;
50+
51+
switch (metadata.framework) {
52+
case Framework.Angular:
53+
return this.angularViewEncapsulationModes[encapsulation];
54+
case Framework.ACX:
55+
return this.acxViewEncapsulationModes[encapsulation];
56+
default:
57+
return undefined;
3958
}
40-
return undefined;
4159
});
4260

4361
readonly changeDetectionStrategy = computed(() => {
44-
const onPush = this.controller()?.directiveHasOnPushStrategy;
45-
return onPush ? 'OnPush' : onPush !== undefined ? 'Default' : undefined;
62+
const metadata = this.controller()?.directiveMetadata;
63+
if (!metadata) return undefined;
64+
65+
const meta = metadata as Partial<AcxDirectiveMetadata | AngularDirectiveMetadata>;
66+
if (meta.onPush !== undefined) {
67+
return meta.onPush ? 'OnPush' : 'Default';
68+
} else {
69+
return undefined;
70+
}
4671
});
4772
}

devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@if (controlsLoaded()) {
22
<mat-accordion cdkDropList (cdkDropListDropped)="drop($event)" [multi]="true">
3-
@let dependencies = controller().directiveMetadata?.dependencies;
4-
@if (dependencies && dependencies.length > 0) {
3+
@let deps = dependencies();
4+
@if (deps && deps.length > 0) {
55
<div class="mat-accordion-content">
66
<mat-expansion-panel [expanded]="true">
77
<mat-expansion-panel-header collapsedHeight="25px" expandedHeight="25px">

0 commit comments

Comments
 (0)