[IMP] devtools: add lifecycle hooks management

This commit adds lifecycle hooks to the interface of the selected
component and allows to inject breakpoints into these hooks.
This commit is contained in:
Julien Carion (juca)
2024-10-24 16:17:32 +02:00
committed by Géry Debongnie
parent aff4019cde
commit ccd31f12d9
4 changed files with 149 additions and 3 deletions
@@ -75,6 +75,19 @@
<ObjectTreeElement object="instance"/> <ObjectTreeElement object="instance"/>
</t> </t>
</div> </div>
<div t-if="store.activeComponent.hooks?.children.length > 0" id="hooks" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'hooks')">
<i class="fa mx-1 pointer-icon"
t-att-class="{'fa-caret-right': !store.activeComponent.hooks.toggled, 'fa-caret-down': store.activeComponent.hooks.toggled}"
/><b>hooks</b>
</div>
<i title="Remove breakpoints" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.removeBreakpoints()"></i>
</div>
<t t-if="store.activeComponent.hooks.toggled" t-foreach="store.activeComponent.hooks.children" t-as="hook" t-key="hook_index">
<ObjectTreeElement object="hook"/>
</t>
</div>
</div> </div>
</t> </t>
</templates> </templates>
@@ -64,6 +64,40 @@ export class ObjectTreeElement extends Component {
show: this.props.object.contentType === "function", show: this.props.object.contentType === "function",
action: () => this.store.inspectFunctionSource(this.props.object.path), action: () => this.store.inspectFunctionSource(this.props.object.path),
}, },
{
title: "Inject breakpoint on component",
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
action: () =>
this.store.injectBreakpoint(this.props.object.name, this.store.activeComponent.path),
},
{
title: "Inject conditional breakpoint on component",
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
action: () => {
const condition = window.prompt("Enter the condition");
if (condition) {
this.store.injectBreakpoint(
this.props.object.name,
this.store.activeComponent.path,
false,
condition
);
}
},
},
{
title: "Inject breakpoint on instance",
show:
this.props.object.contentType === "array" &&
this.props.object.objectType === "hook" &&
!["mounted", "willStart"].includes(this.props.object.name),
action: () =>
this.store.injectBreakpoint(
this.props.object.name,
this.store.activeComponent.path,
true
),
},
]; ];
} }
@@ -33,6 +33,7 @@ export const store = reactive({
props: { toggled: true, children: [] }, props: { toggled: true, children: [] },
env: { toggled: false, children: [] }, env: { toggled: false, children: [] },
instance: { toggled: true, children: [] }, instance: { toggled: true, children: [] },
hooks: { toggled: true, children: [] },
version: "1.0", version: "1.0",
}, },
selectedElement: null, selectedElement: null,
@@ -564,6 +565,21 @@ export const store = reactive({
} }
}, },
async injectBreakpoint(hook, path, instanceOnly = false, condition = "1") {
path = [...path];
await evalFunctionInWindow(
"injectBreakpoint",
[hook, path, instanceOnly, condition],
this.activeFrame
);
await this.loadComponentsTree(true);
},
async removeBreakpoints() {
await evalFunctionInWindow("removeBreakpoints", [], this.activeFrame);
await this.loadComponentsTree(true);
},
// Trigger the highlight on the component in the page // Trigger the highlight on the component in the page
highlightComponent(path) { highlightComponent(path) {
evalFunctionInWindow("highlightComponent", [path], this.activeFrame); evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
@@ -51,6 +51,8 @@
this.requestedFrame = false; this.requestedFrame = false;
this.enabledSelector = false; this.enabledSelector = false;
this.eventsBatch = []; this.eventsBatch = [];
this.breakpointsClassMap = new Map();
this.breakpointsHookMap = new Map();
// Object which defines how different types of data should be displayed when passed to the devtools // Object which defines how different types of data should be displayed when passed to the devtools
this.serializer = { this.serializer = {
// Defines how leaf object nodes should be displayed in the extension when inside a bigger structure // Defines how leaf object nodes should be displayed in the extension when inside a bigger structure
@@ -744,6 +746,9 @@
} }
// Path to the component node is only strings, becomes objects for properties // Path to the component node is only strings, becomes objects for properties
const index = path.findIndex((key) => typeof key !== "string"); const index = path.findIndex((key) => typeof key !== "string");
if (index === -1) {
return this.getComponentNode(path);
}
const componentNode = this.getComponentNode(path.slice(0, index)); const componentNode = this.getComponentNode(path.slice(0, index));
const obj = this.getObject(componentNode, path.slice(index)); const obj = this.getObject(componentNode, path.slice(index));
return obj; return obj;
@@ -896,12 +901,15 @@
path.shift(); path.shift();
} }
// the value is either "props" or "env" here // the value is either "props" or "env" here
if (objType !== "instance") { if (objType !== "instance" && objType !== "hook") {
obj = oldTree[path[0].value].children; obj = oldTree[path[0].value].children;
path.shift(); path.shift();
// there is nothing otherwise but extension side it is in instance // there is nothing otherwise but extension side it is in instance
} else { } else if (objType === "instance") {
obj = oldTree.instance.children; obj = oldTree.instance.children;
} else {
obj = oldTree.hooks.children;
path.shift();
} }
// the first element here is directly in an array instead of a children array // the first element here is directly in an array instead of a children array
obj = obj[path[0].childIndex]; obj = obj[path[0].childIndex];
@@ -1377,6 +1385,39 @@
component.subscriptions.children.push(subscription); component.subscriptions.children.push(subscription);
}); });
} }
// Load hooks of the component
if (!isApp) {
component.hooks = { toggled: oldTree ? oldTree.hooks.toggled : true, children: [] };
const hooksList = [
"mounted",
"patched",
"willDestroy",
"willPatch",
"willStart",
"willUnmount",
"willUpdateProps",
];
const hooksPath = [...instancePath, { type: "item", value: "__owl__" }];
Reflect.ownKeys(instance.__owl__)
.sort(compareKeys)
.forEach((key) => {
if (hooksList.includes(key)) {
let oldBranch = oldTree?.hooks.children[component.hooks.children.length];
const property = this.serializeObjectChild(
instance.__owl__,
{ type: "item", value: key, childIndex: component.hooks.children.length },
0,
"hook",
hooksPath,
oldBranch,
oldTree
);
if (property) {
component.hooks.children.push(property);
}
}
});
}
return component; return component;
} }
// Replace the content of a parsed getter object with the result of the corresponding get method // Replace the content of a parsed getter object with the result of the corresponding get method
@@ -1714,6 +1755,48 @@
} }
} }
injectBreakpoint(hook, path, instanceOnly, condition) {
const componentNode = this.getObjectProperty(path);
const injectFunctionInHook = (comp, hook, fn) => {
comp[hook].push(fn);
};
const originalHook = [...componentNode.component.__owl__[hook]];
injectFunctionInHook(componentNode.component.__owl__, hook, () => {
debugger;
});
if (!this.breakpointsHookMap.get([componentNode.component.__owl__, hook])) {
this.breakpointsHookMap.set([componentNode.component.__owl__, hook], originalHook);
}
if (!instanceOnly) {
const componentClass = componentNode.component.constructor;
const originalSetup = componentClass.prototype.setup;
if (!this.breakpointsClassMap.get(componentClass)) {
this.breakpointsClassMap.set(componentClass, originalSetup);
}
componentClass.prototype.setup = function () {
const debuggerFunc = () => {
if (eval(condition)) {
this;
debugger;
}
};
injectFunctionInHook(this.__owl__, hook, debuggerFunc);
originalSetup.call(this, ...arguments);
};
}
}
removeBreakpoints() {
for (const [component, setup] of this.breakpointsClassMap) {
component.prototype.setup = setup;
}
this.breakpointsClassMap.clear();
for (const [ref, originalHook] of this.breakpointsHookMap) {
ref[0][ref[1]] = originalHook;
}
this.breakpointsHookMap.clear();
}
targetName(target, node) { targetName(target, node) {
// check on component // check on component
const { component } = node; const { component } = node;