From ccd31f12d9c14612539b13debbf979531d3cbb07 Mon Sep 17 00:00:00 2001 From: "Julien Carion (juca)" Date: Thu, 24 Oct 2024 16:17:32 +0200 Subject: [PATCH] [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. --- .../details_window/details_window.xml | 15 +++- .../object_tree_element.js | 34 ++++++++ .../devtools/src/devtools_app/store/store.js | 16 ++++ .../page_scripts/owl_devtools_global_hook.js | 87 ++++++++++++++++++- 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml index 2007adea..34deaaf9 100644 --- a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml +++ b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml @@ -59,7 +59,7 @@ -
+
+
+
+
+ hooks +
+ +
+ + + +
diff --git a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js index 0cd37a0f..9a2b7947 100644 --- a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js +++ b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js @@ -64,6 +64,40 @@ export class ObjectTreeElement extends Component { show: this.props.object.contentType === "function", 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 + ), + }, ]; } diff --git a/tools/devtools/src/devtools_app/store/store.js b/tools/devtools/src/devtools_app/store/store.js index e2d493fd..80808b73 100644 --- a/tools/devtools/src/devtools_app/store/store.js +++ b/tools/devtools/src/devtools_app/store/store.js @@ -33,6 +33,7 @@ export const store = reactive({ props: { toggled: true, children: [] }, env: { toggled: false, children: [] }, instance: { toggled: true, children: [] }, + hooks: { toggled: true, children: [] }, version: "1.0", }, 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 highlightComponent(path) { evalFunctionInWindow("highlightComponent", [path], this.activeFrame); diff --git a/tools/devtools/src/page_scripts/owl_devtools_global_hook.js b/tools/devtools/src/page_scripts/owl_devtools_global_hook.js index 2bf33b5d..7f738a88 100644 --- a/tools/devtools/src/page_scripts/owl_devtools_global_hook.js +++ b/tools/devtools/src/page_scripts/owl_devtools_global_hook.js @@ -51,6 +51,8 @@ this.requestedFrame = false; this.enabledSelector = false; 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 this.serializer = { // 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 const index = path.findIndex((key) => typeof key !== "string"); + if (index === -1) { + return this.getComponentNode(path); + } const componentNode = this.getComponentNode(path.slice(0, index)); const obj = this.getObject(componentNode, path.slice(index)); return obj; @@ -896,12 +901,15 @@ path.shift(); } // the value is either "props" or "env" here - if (objType !== "instance") { + if (objType !== "instance" && objType !== "hook") { obj = oldTree[path[0].value].children; path.shift(); // there is nothing otherwise but extension side it is in instance - } else { + } else if (objType === "instance") { 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 obj = obj[path[0].childIndex]; @@ -1377,6 +1385,39 @@ 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; } // 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) { // check on component const { component } = node;