Files
owl/tools/devtools/src/page_scripts/owl_devtools_global_hook.js
T
Julien Carion (juca) 7078d64049 [IMP] devtools: observe variables
This commit allows the user to observe variables so that their content
will be refreshed every 200ms and will be displayed as long as they
are found in the tree.
2024-12-05 15:43:39 +01:00

1993 lines
71 KiB
JavaScript

(function () {
"use strict";
const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1;
class OwlDevtoolsGlobalHook {
constructor() {
// The set of apps exposed by owl
this.apps = window.__OWL_DEVTOOLS__.apps;
// Class definition of an owl Fiber
this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
// Same but for RootFiber
this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
// This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive
// in __OWL_DEVTOOLS__
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
// Set to keep track of the HTML elements we added to the page
this.addedElements = [];
// To keep track of the succession order of the render events
this.eventId = 0;
// Set to keep track of the frame on which this script is loaded
this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page
const iFrameObserver = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) {
if (addedNode.tagName == "IFRAME") {
// Ensure it is not an empty iframe element
if (addedNode.contentDocument) {
/*
* This message is intercepted by the content script which relays it to the background script which relays it to the devtools app.
* This process may seem long and indirect but is necessary. This applies to all window.top.postMessage methods in this file.
* More information in the docs: https://developer.chrome.com/docs/extensions/mv3/devtools/#evaluated-scripts-to-devtools
*/
window.top.postMessage({
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
});
}
}
});
});
});
iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.patchAppsSetMethods();
this.recordEvents = false;
this.traceRenderings = false;
this.traceSubscriptions = false;
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
// This is also used by default when it is a standalone item (like functions, numbers, strings, etc)
// The asConstructorName parameter can be passed to change the display of objects and functions to
// be their constructor name (useful for prototype, map and set display)
serializeItem(value, asConstructorName = false) {
if (typeof value === "object") {
if (value == null) {
return "null";
}
if (asConstructorName) {
return value.constructor.name;
}
if (value instanceof Array) {
return "Array(" + value.length + ")";
}
return "{...}";
} else if (typeof value === "undefined") {
return "undefined";
} else if (typeof value === "string") {
return JSON.stringify(value);
} else if (typeof value === "function") {
if (asConstructorName) {
return value.constructor.name;
}
let functionString = value.toString();
if (functionString.startsWith("class")) {
return "class " + value.name;
} else {
// This replaces the body of any function with curly braces by ...
const regex = /\{(?![^()]*\))(.*?)\}(?![^{}]*\})/s;
return functionString.replace(regex, "{...}");
}
} else {
let valueAsString = value.toString();
if (asConstructorName && valueAsString.length > 10) {
valueAsString = valueAsString.substring(0, 8) + "...";
}
return valueAsString;
}
},
array(arr) {
const result = [];
let length = 0;
for (const item of arr) {
if (length > 25) {
result.push("...");
break;
}
const element = this.serializeItem(item);
length += element.length;
result.push(element);
}
return "[" + result.join(", ") + "]";
},
object(obj) {
const result = [];
let length = 0;
if (obj instanceof String) {
result[0] = `'${obj.toString()}'`;
} else if (obj instanceof Array) {
return `${obj.constructor.name} ${this.array([...obj])}`;
} else if (obj instanceof Number) {
result[0] = obj.toString();
} else {
for (const key of Reflect.ownKeys(obj)) {
if (length > 25) {
result.push("...");
break;
}
let element;
if (Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")) {
element = key.toString() + ": (...)";
} else {
element = key.toString() + ": " + this.serializeItem(obj[key]);
}
length += element.length;
result.push(element);
}
for (const key of Object.getOwnPropertySymbols(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key.toString() + ": " + this.serializeItem(obj[key]);
length += element.length;
result.push(element);
}
}
if (obj.constructor && obj.constructor.name !== "Object") {
return obj.constructor.name + " {" + result.join(", ") + "}";
}
return "{" + result.join(", ") + "}";
},
map(obj) {
const result = [];
let length = 0;
for (const [key, value] of obj.entries()) {
if (length > 25) {
result.push("...");
break;
}
const element =
this.serializeItem(key, true) + " => " + this.serializeItem(value, true);
length += element.length;
result.push(element);
}
return "Map(" + obj.size + "){" + result.join(", ") + "}";
},
["map entry"](obj) {
return (
"{" + this.serializeItem(obj[0], true) + " => " + this.serializeItem(obj[1], true) + "}"
);
},
set(obj) {
const result = [];
let length = 0;
for (const value of obj) {
if (length > 25) {
result.push("...");
break;
}
const element = this.serializeItem(value, false);
length += element.length;
result.push(element);
}
return "Set(" + obj.size + "){" + result.join(", ") + "}";
},
// Returns a shortened string version of a collection of properties for display purposes
// Shortcut for serializeItem when it is a standalone item
serializeContent(item, type) {
if (this[type]) {
return this[type](item);
}
return this.serializeItem(item);
},
};
}
initDevtools(frame = "top") {
if (!this.devtoolsInit) {
document.addEventListener("mouseover", this.HTMLSelector, { capture: true });
this.frame = frame;
const self = this;
// Flush the events batcher when a root render is completed
const original_Complete = self.RootFiber.prototype.complete;
self.RootFiber.prototype.complete = function () {
original_Complete.call(this, ...arguments);
const path = self.getComponentPath(this.node);
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
window.top.postMessage({
source: "owl-devtools",
type: "Complete",
data: path,
origin: { frame: self.frame },
});
if (self.recordEvents) {
window.top.postMessage({
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
});
self.eventsBatch = [];
}
};
this.devtoolsInit = true;
}
}
// Modify the methods of the apps set in order to send a message each time it is modified.
patchAppsSetMethods() {
const originalAdd = this.apps.add;
const originalDelete = this.apps.delete;
this.apps.add = function () {
originalAdd.call(this, ...arguments);
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
};
this.apps.delete = function () {
originalDelete.call(this, ...arguments);
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
};
}
// Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() {
let app;
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
if (!app.root) {
return;
}
const self = this;
const originalFlush = app.scheduler.constructor.prototype.flush;
let inFlush = false;
let _render = false;
app.scheduler.constructor.prototype.flush = function () {
// Used to know when a render is triggered inside the flush method or not
inFlush = true;
originalFlush.call(this, ...arguments);
inFlush = false;
};
const originalRender = self.Fiber.prototype.render;
self.Fiber.prototype.render = function () {
const id = self.eventId++;
_render = false;
let flushed = false;
// We know if a render comes from flush before calling the render method
if (this instanceof self.RootFiber && inFlush) {
flushed = true;
}
// patch the renderFn function from the node to only measure the time
// spent in the template, not in additional work, such as flushing scheduler
const node = this.node;
const nodeRenderFn = node.renderFn;
let time = 0;
this.node.renderFn = function () {
const before = performance.now();
const result = nodeRenderFn.call(this);
time = performance.now() - before;
// unpatch the node render function
node.renderFn = nodeRenderFn;
return result;
};
originalRender.call(this, ...arguments);
if (_render && self.traceRenderings && this instanceof self.RootFiber) {
console.groupCollapsed(`Rendering <${this.node.name}>`);
console.trace();
console.groupEnd();
}
if (self.recordEvents) {
const path = self.getComponentPath(this.node);
// if the render comes from flush
if (flushed) {
self.eventsBatch.push({
type: "render (flushed)",
component: this.node.name,
key: this.node.parentKey ? this.node.parentKey : "",
path: path,
time: time,
id: id,
});
// if _render is called, it is a proper render (and not a delayed one)
} else if (_render) {
// A render on a RootFiber is a root render and can propagate other renders to its children
if (this instanceof self.RootFiber) {
self.eventsBatch.push({
type: this.deep ? "render (deep)" : "render",
component: this.node.name,
key: this.node.parentKey ? this.node.parentKey : "",
path: path,
time: time,
id: id,
});
// if the node status is NEW, the node has been created just before rendering
} else if (this.node.status === 0) {
self.eventsBatch.push({
type: "create",
component: this.node.name,
key: this.node.parentKey ? this.node.parentKey : "",
path: path,
time: time,
id: id,
});
// else it is an update
} else {
self.eventsBatch.push({
type: "update",
component: this.node.name,
key: this.node.parentKey ? this.node.parentKey : "",
path: path,
time: time,
id: id,
});
}
// _render has not been called so it is a delayed render that could be flushed later on
} else {
self.eventsBatch.push({
type: "render (delayed)",
component: this.node.name,
key: this.node.parentKey ? this.node.parentKey : "",
path: path,
time: time,
id: id,
});
}
}
};
const original_Render = self.Fiber.prototype._render;
self.Fiber.prototype._render = function () {
_render = true;
original_Render.call(this, ...arguments);
};
// Signals when a component is destroyed
const originalDestroy = app.root.constructor.prototype._destroy;
app.root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
}
// patch reactivity system to activate subscription tracing
patchReactivity() {
// i am simultaneously proud and ashamed of this code...
const WeakMapGet = WeakMap.prototype.get;
const MapGet = Map.prototype.get;
const SetAdd = Set.prototype.add;
const self = this;
let callbacksToTargets;
let targetToKeysToCallbacks;
// Step 1: extract internal values from owl
const obj = self.reactive({}, () => {});
let count = 0;
WeakMap.prototype.get = function () {
count++;
if (count === 1) {
targetToKeysToCallbacks = this;
}
if (count === 3) {
callbacksToTargets = this;
WeakMap.prototype.get = WeakMapGet;
}
return WeakMapGet.call(this, ...arguments);
};
obj.a;
// Step 2: patch these objects
let key;
function getKey(_key) {
key = _key;
return MapGet.call(this, _key);
}
targetToKeysToCallbacks.get = function (target) {
const keyToCallbacks = WeakMapGet.call(this, target);
if (keyToCallbacks) {
keyToCallbacks.get = getKey;
}
return keyToCallbacks;
};
function addTarget(target) {
// HERE
if (self.traceSubscriptions && !this.has(target)) {
console.groupCollapsed(`Observing:`, key, target);
console.trace();
console.groupEnd();
}
return SetAdd.call(this, target);
}
callbacksToTargets.get = function (callback) {
const targets = WeakMapGet.call(this, callback);
if (targets) {
targets.add = addTarget;
}
return targets;
};
}
toggleTracing(value) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.traceRenderings = value;
return this.traceRenderings;
}
toggleSubscriptionTracing(value) {
if (value) {
this.patchReactivity();
this.patchReactivity = () => {}; // to only patch once
}
this.traceSubscriptions = value;
return value;
}
// Enables/disables the recording of the render/destroy events based on value
toggleEventsRecording(value, index) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.recordEvents = value;
this.eventId = index;
return this.recordEvents;
}
// Reset the event ids (the events on devtools side will be cleared at the same time)
resetEvents() {
this.eventId = 0;
}
// Get the urls of all iframes present on the page
getIFrameUrls() {
let frames = [];
for (const frame of document.querySelectorAll("iframe")) {
frames.push(frame.contentDocument.location.href);
}
return frames;
}
// Draws a highlighting rectangle on the specified html element and displays its dimensions and the specified name in a box
highlightElements(elements, name) {
this.removeHighlights();
if (elements.length === 0) {
return;
}
let minTop = Number.MAX_SAFE_INTEGER;
let minLeft = Number.MAX_SAFE_INTEGER;
let maxBottom = Number.MIN_SAFE_INTEGER;
let maxRight = Number.MIN_SAFE_INTEGER;
for (const element of elements) {
let rect;
// Since this function accepts text nodes, we need to create a range to get the position and dimensions
if (element instanceof Text) {
const range = document.createRange();
range.selectNode(element);
rect = range.getBoundingClientRect();
} else {
rect = element.getBoundingClientRect();
}
const top = rect.top;
const left = rect.left;
const bottom = top + rect.height;
const right = left + rect.width;
minTop = Math.min(minTop, top);
minLeft = Math.min(minLeft, left);
maxBottom = Math.max(maxBottom, bottom);
maxRight = Math.max(maxRight, right);
const width = right - left;
const height = bottom - top;
if (element instanceof HTMLElement) {
const marginTop = parseInt(getComputedStyle(element).marginTop);
const marginRight = parseInt(getComputedStyle(element).marginRight);
const marginBottom = parseInt(getComputedStyle(element).marginBottom);
const marginLeft = parseInt(getComputedStyle(element).marginLeft);
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
const paddingRight = parseInt(getComputedStyle(element).paddingRight);
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
const paddingLeft = parseInt(getComputedStyle(element).paddingLeft);
const highlight = document.createElement("div");
highlight.style.top = `${top}px`;
highlight.style.left = `${left}px`;
highlight.style.width = `${width}px`;
highlight.style.height = `${height}px`;
highlight.style.boxSizing = "border-box";
highlight.style.position = "fixed";
highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
highlight.style.borderStyle = "solid";
highlight.style.borderWidth = `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`;
highlight.style.borderColor = "rgba(65, 196, 68, 0.4)";
highlight.style.zIndex = "10000";
highlight.style.pointerEvents = "none";
document.body.appendChild(highlight);
this.addedElements.push(highlight);
const highlightMargins = document.createElement("div");
highlightMargins.style.top = `${top - marginTop}px`;
highlightMargins.style.left = `${left - marginLeft}px`;
highlightMargins.style.width = `${width + marginLeft + marginRight}px`;
highlightMargins.style.height = `${height + marginBottom + marginTop}px`;
highlightMargins.style.position = "fixed";
highlightMargins.style.borderStyle = "solid";
highlightMargins.style.boxSizing = "border-box";
highlightMargins.style.borderWidth = `${marginTop}px ${marginRight}px ${marginBottom}px ${marginLeft}px`;
highlightMargins.style.borderColor = "rgba(241, 179, 121, 0.4)";
highlightMargins.style.zIndex = "10000";
highlightMargins.style.pointerEvents = "none";
document.body.appendChild(highlightMargins);
this.addedElements.push(highlightMargins);
// In case the element is a range because a text node was passed
} else {
const highlight = document.createElement("div");
highlight.style.top = `${top}px`;
highlight.style.left = `${left}px`;
highlight.style.width = `${width}px`;
highlight.style.height = `${height}px`;
highlight.style.boxSizing = "border-box";
highlight.style.position = "fixed";
highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
highlight.style.zIndex = "10000";
highlight.style.pointerEvents = "none";
document.body.appendChild(highlight);
this.addedElements.push(highlight);
}
}
const width = maxRight - minLeft;
const height = maxBottom - minTop;
const detailsBox = document.createElement("div");
detailsBox.className = "owl-devtools-detailsBox";
detailsBox.innerHTML = `
<div style="color: #ffc107; display: inline;">${name} </div><div style="color: white; display: inline;">${width.toFixed(
2
)}px x ${height.toFixed(2)}px</div>
`;
detailsBox.style.position = "fixed";
detailsBox.style.backgroundColor = "black";
detailsBox.style.padding = "5px";
detailsBox.style.zIndex = "10000";
detailsBox.style.pointerEvents = "none";
detailsBox.style.display = "inline";
document.body.appendChild(detailsBox);
this.addedElements.push(detailsBox);
const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const viewportHeight = Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
);
const detailsBoxRect = detailsBox.getBoundingClientRect();
let detailsBoxTop = minTop + height + 5;
let detailsBoxLeft = minLeft;
if (detailsBoxTop + detailsBoxRect.height > viewportHeight) {
detailsBoxTop = minTop - detailsBoxRect.height - 5;
if (detailsBoxTop < 0) {
detailsBoxTop = minTop;
}
}
if (detailsBoxLeft + detailsBoxRect.width > viewportWidth) {
detailsBoxLeft = minLeft - detailsBoxRect.width - 5;
}
detailsBox.style.top = `${detailsBoxTop}px`;
detailsBox.style.left = `${detailsBoxLeft + 5}px`;
}
// Remove all elements drawn by the HighlightElement function
removeHighlights = () => {
for (const el of this.addedElements) {
el.remove();
}
this.addedElements = [];
};
// Identify the hovered component based on the corresponding DOM element and send the Select message
// when the target changes
HTMLSelector = (ev) => {
if (this.enabledSelector) {
const target = ev.target;
if (!this.currentSelectedElement || !target.isEqualNode(this.currentSelectedElement)) {
const path = this.getElementPath(target);
this.highlightComponent(path);
this.currentSelectedElement = target;
window.top.postMessage({
source: "owl-devtools",
type: "SelectElement",
data: path,
});
}
} else {
this.removeHighlights();
}
};
// Activate the HTML selector tool
enableHTMLSelector() {
this.enabledSelector = true;
document.addEventListener("click", this.disableHTMLSelector, { capture: true });
document.addEventListener("scroll", this.removeHighlights, { capture: true });
}
// Diasble the HTML selector tool
disableHTMLSelector = (ev = undefined) => {
this.enabledSelector = false;
if (ev) {
if (!ev.isTrusted) {
return;
}
ev.stopPropagation();
ev.preventDefault();
}
this.removeHighlights();
document.removeEventListener("click", this.disableHTMLSelector, { capture: true });
document.removeEventListener("scroll", this.removeHighlights, { capture: true });
window.top.postMessage({
source: "owl-devtools",
type: "StopSelector",
});
};
// Returns the object specified by the path starting from the topParent object
getObject(topParent, path) {
let obj = topParent;
if (path.length === 0) {
return obj;
}
for (const key of path) {
switch (key.type) {
case "prototype":
obj = Object.getPrototypeOf(obj);
break;
// We will be handling maps and sets entries as arrays to keep things stable
case "set entries":
case "map entries":
obj = [...obj];
break;
// Kind of tricky structure-wise but the set entry already is the object itself so this is
// only useful for display purpose in the devtools.
case "set value":
obj = obj;
break;
// A map entry is a two items array with the key and the value so indexes 0 and 1
case "map key":
obj = obj[0];
break;
case "map value":
obj = obj[1];
break;
case "getter":
// When the item is a getter, get its value.
try {
obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
} catch (e) {
obj = "Exception: " + e.toString();
}
break;
case "prototype getter":
case "set entry":
case "map entry":
case "item":
// When the item is a symbol, we make sure it is the right one by using the index on the
// Object.getOwnPropertySymbols array.
if (key.hasOwnProperty("symbolIndex") && key.symbolIndex >= 0) {
const symbol = Object.getOwnPropertySymbols(obj)[key.symbolIndex];
if (symbol) {
obj = obj[symbol];
}
} else if (Object.getOwnPropertyDescriptor(obj, key.value)?.hasOwnProperty("get")) {
try {
obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
} catch (e) {
obj = "Exception: " + e.toString();
}
} else {
try {
obj = obj[key.value];
} catch (e) {
obj = "Exception: " + e.toString();
}
}
}
if (obj) {
obj = this.toRaw(obj);
}
}
return obj;
}
// Returns the asked property given its global path
getObjectProperty(path) {
// Just return the corresponding app if path is of length 1
if (path.length === 1) {
return [...this.apps][path[0]];
}
// 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(index === 1 ? path[0] : path.slice(0, index));
if (componentNode) {
return this.getObject(componentNode, path.slice(index));
}
return "Exception: component not found";
}
// Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component
// parentObj is the parent of the node, key is the path key of the node, type is where the object belongs to (props, env, instance, ...),
// oldBranch is supposedly the same final node in the previous version of the tree and oldTree is the complete old component details
serializeObjectChild(parentObj, key, depth, type, path, oldBranch, oldTree) {
let obj;
let child = {
depth: depth,
toggled: false,
objectType: type,
hasChildren: false,
};
if (oldBranch?.toggled) {
child.toggled = true;
}
child.path = path.concat([key]);
child.children = [];
switch (key.type) {
case "prototype":
child.name = "[[Prototype]]";
child.contentType = "object";
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
child.hasChildren = true;
if (!oldTree && type === "env" && Object.getPrototypeOf(parentObj) !== Object.prototype) {
child.toggled = true;
}
break;
case "set entries":
case "map entries":
child.name = "[[Entries]]";
child.content = "";
child.contentType = "array";
child.hasChildren = true;
break;
case "set value":
child.name = "value";
obj = parentObj;
break;
case "map key":
child.name = "key";
obj = parentObj[0];
break;
case "map value":
child.name = "value";
obj = parentObj[1];
break;
case "getter":
child.name = key.value;
obj = parentObj[key.value];
break;
case "prototype getter":
case "set entry":
case "map entry":
case "item":
if (Object.getOwnPropertyDescriptor(parentObj, key.value)?.hasOwnProperty("get")) {
child.name = "get " + key.value;
obj = Object.getOwnPropertyDescriptor(parentObj, key.value).get;
} else {
child.name = key.value;
if (typeof key.value === "symbol") {
child.name = key.value.toString();
obj = parentObj[key.value];
child.path[child.path.length - 1].symbolIndex = Object.getOwnPropertySymbols(
parentObj
).findIndex((sym) => sym === key.value);
child.path[child.path.length - 1].value = key.value.toString();
} else if (key.hasOwnProperty("symbolIndex")) {
obj = parentObj[Object.getOwnPropertySymbols(parentObj)[key.symbolIndex]];
} else {
obj = parentObj[key.value];
}
}
if (key.type === "map entry") {
child.contentType = "array";
child.hasChildren = obj.length > 0;
child.content = this.serializer.serializeContent(obj, key.type);
}
break;
}
if (!child.contentType) {
if (obj === null) {
child.content = "null";
child.contentType = "object";
child.hasChildren = false;
} else if (obj === undefined) {
child.content = "undefined";
child.contentType = "undefined";
child.hasChildren = false;
} else {
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
child.contentType = "map";
child.hasChildren = true;
break;
case obj instanceof Set:
child.contentType = "set";
child.hasChildren = true;
break;
case obj.constructor?.name === "Array":
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
child.contentType = "function";
child.hasChildren = true;
break;
case typeof obj === "object":
child.contentType = "object";
child.hasChildren =
Object.keys(obj).length > 0 ||
Object.getOwnPropertySymbols(obj).length > 0 ||
obj.constructor.name !== "Object";
break;
default:
child.contentType = typeof obj;
child.hasChildren = false;
}
if (key.type === "set entry") {
child.content = this.serializer.serializeItem(obj, true);
} else {
child.content = this.serializer.serializeContent(obj, child.contentType);
}
}
}
if (child.toggled) {
child.children = this.loadObjectChildren(
child.path,
child.depth,
child.contentType,
child.objectType,
oldTree
);
}
this.addHighlightedKeys(child);
return child;
}
// returns the serialized node corresponding to its path in the serialized tree
getObjectInOldTree(oldTree, completePath, objType) {
const objPathIndex = completePath.findIndex((key) => typeof key !== "string");
let path = completePath.slice(objPathIndex);
let obj;
if (objType === "subscription") {
const subscriptionPath = completePath.slice(0, objPathIndex + 3);
obj = oldTree.subscriptions.children.find(
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
).target;
path = path.slice(3);
} else {
// Everything here is in component if it is not an app so remove this key of the path in the former case
if (objPathIndex > 1) {
path.shift();
}
// the value is either "props" or "env" here
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 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];
path.shift();
}
// just follow the indexes in the rest of the path
for (const key of path) {
obj = obj.children[key.childIndex];
}
return obj;
}
// Returns a serialized version of the children properties of the specified component's property given its path.
loadObjectChildren(path, depth, contentType, objType, oldTree) {
const children = [];
depth = depth + 1;
let obj = this.getObjectProperty(path);
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
if (!obj) {
return [];
}
let index = 0;
const lastKey = path.at(-1);
let prototype;
if (lastKey.type.includes("entry")) {
if (lastKey.type === "map entry") {
const mapKey = this.serializeObjectChild(
obj,
{ type: "map key", childIndex: 0 },
depth,
objType,
path,
oldBranch?.children[0],
oldTree
);
children.push(mapKey);
const mapValue = this.serializeObjectChild(
obj,
{ type: "map value", childIndex: 1 },
depth,
objType,
path,
oldBranch?.children[1],
oldTree
);
children.push(mapValue);
} else if (lastKey.type === "set entry") {
const setValue = this.serializeObjectChild(
obj,
{ type: "set value", childIndex: 0 },
depth,
objType,
path,
oldBranch?.children[0],
oldTree
);
children.push(setValue);
}
return children;
}
switch (contentType) {
case "array":
if (lastKey.type.includes("entries")) {
for (; index < obj.length; index++) {
const child = this.serializeObjectChild(
obj,
{
type: lastKey.type.replace("entries", "entry"),
value: index,
childIndex: children.length,
},
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) {
children.push(child);
}
}
} else {
for (; index < obj.length; index++) {
const child = this.serializeObjectChild(
obj,
{ type: "item", value: index, childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) {
children.push(child);
}
}
}
break;
case "set":
case "map":
const entries = this.serializeObjectChild(
obj,
{ type: contentType + " entries", value: "[[Entries]]", childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (entries) {
children.push(entries);
index++;
}
Reflect.ownKeys(obj).forEach((key) => {
const child = this.serializeObjectChild(
obj,
{ type: "item", value: key, childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) {
children.push(child);
}
index++;
});
break;
case "object":
case "function":
Reflect.ownKeys(obj)
.sort(compareKeys)
.forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: depth,
toggled: false,
objectType: objType,
path: [...path, { type: "getter", value: key, childIndex: children.length }],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
children.push(child);
}
const child = this.serializeObjectChild(
obj,
{ type: "item", value: key, childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) {
children.push(child);
}
index++;
});
}
let proto = Object.getPrototypeOf(obj);
while (proto) {
Reflect.ownKeys(proto).forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(proto, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: depth,
toggled: false,
objectType: objType,
path: [
...path,
{ type: "prototype getter", value: key, childIndex: children.length },
],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
children.push(child);
}
});
proto = Object.getPrototypeOf(proto);
}
if (obj.__proto__) {
prototype = this.serializeObjectChild(
obj,
{ type: "prototype", childIndex: children.length },
depth,
objType,
path,
oldBranch?.children.at(-1),
oldTree
);
children.push(prototype);
}
return children;
}
// Returns the Component node given its path and the root component node
getComponentNode(path) {
// The node is an app and not a component
if (path.length === 1) {
return [...this.apps][path[0]];
}
// The second element in the path will always be the root of the app
let node = [...this.apps][path[0]]?.root;
if (path instanceof Array) {
if (!node) {
return null;
}
for (let i = 2; i < path.length; i++) {
// From this point onwards, it is an object path inside the component node
if (typeof path[i] !== "string") {
break;
}
if (node.children.hasOwnProperty(path[i])) {
node = node.children[path[i]];
} else {
return null;
}
}
} else {
const simplifiedPathArray = path.split("/");
if (node.name !== simplifiedPathArray[1]) {
return null;
}
for (let i = 2; i < simplifiedPathArray.length; i += 2) {
const key = Reflect.ownKeys(node.children)[simplifiedPathArray[i]];
node = node.children[key];
if (node.name !== simplifiedPathArray[i + 1]) {
return null;
}
}
}
return node;
}
// Apply manual render to the specified component
refreshComponent(path) {
const componentNode = this.getComponentNode(path);
componentNode.render();
}
// Returns the component's details given its path
getComponentDetails(path = null, oldTree = null) {
let component = {};
if (!path) {
path = this.getElementPath($0);
}
component.path = path;
let node = this.getComponentNode(path) || [...this.apps].find((app) => app.root)?.root;
if (!node) {
return null;
}
// A path with only the app index indicates that the component is an App instead
const isApp = path.length === 1;
if (isApp) {
component.version = node.__proto__.constructor?.version
? node.__proto__.constructor.version
: "<2.0.8";
}
// Load props of the component
const props = isApp ? node.props : node.component.props;
component.props = { toggled: oldTree ? oldTree.props.toggled : true, children: [] };
component.name = isApp
? node?.name
? `App (${node.name})`
: "App " + (Number(path[0]) + 1)
: node.component.constructor.name;
const propsPath = isApp
? [...path, { type: "item", value: "props" }]
: [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
Reflect.ownKeys(props)
.sort(compareKeys)
.forEach((key) => {
let oldBranch = oldTree?.props.children[component.props.children.length];
const property = this.serializeObjectChild(
props,
{ type: "item", value: key, childIndex: component.props.children.length },
0,
"props",
propsPath,
oldBranch,
oldTree
);
if (property) {
component.props.children.push(property);
}
});
// Load env of the component
const env = isApp ? node.env : node.component.env;
component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
const envPath = isApp
? [...path, { type: "item", value: "env" }]
: [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
Reflect.ownKeys(env)
.sort(compareKeys)
.forEach((key) => {
let oldBranch = oldTree?.env.children[component.env.children.length];
const envElement = this.serializeObjectChild(
env,
{ type: "item", value: key, childIndex: component.env.children.length },
0,
"env",
envPath,
oldBranch,
oldTree
);
if (envElement) {
component.env.children.push(envElement);
}
});
// Load env getters
let obj = Object.getPrototypeOf(env);
Reflect.ownKeys(obj).forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: 0,
toggled: false,
objectType: "env",
path: [
...envPath,
{ type: "prototype getter", value: key, childIndex: component.env.children.length },
],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
component.env.children.push(child);
}
});
const envPrototype = this.serializeObjectChild(
env,
{ type: "prototype", childIndex: component.env.children.length },
0,
"env",
envPath,
oldTree?.env[component.env.children.length],
oldTree
);
component.env.children.push(envPrototype);
// Load instance of the component
const instance = isApp ? node : node.component;
component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
Reflect.ownKeys(instance)
.sort(compareKeys)
.forEach((key) => {
if (!["env", "props"].includes(key)) {
let oldBranch = oldTree?.instance.children[component.instance.children.length];
const instanceElement = this.serializeObjectChild(
instance,
{ type: "item", value: key, childIndex: component.instance.children.length },
0,
"instance",
instancePath,
oldBranch,
oldTree
);
if (instanceElement) {
component.instance.children.push(instanceElement);
}
}
});
// Load instance getters
obj = Object.getPrototypeOf(instance);
while (obj) {
Reflect.ownKeys(obj).forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: 0,
toggled: false,
objectType: "instance",
path: [
...instancePath,
{
type: "prototype getter",
value: key,
childIndex: component.instance.children.length,
},
],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
component.instance.children.push(child);
}
});
obj = Object.getPrototypeOf(obj);
}
const instancePrototype = this.serializeObjectChild(
instance,
{ type: "prototype", childIndex: component.instance.children.length },
0,
"instance",
instancePath,
oldTree?.instance[component.instance.children.length],
oldTree
);
component.instance.children.push(instancePrototype);
// Load subscriptions of the component
if (isApp) {
component.subscriptions = {
toggled: oldTree ? oldTree.subscriptions.toggled : true,
children: [],
};
} else {
const rawSubscriptions = this.topLevelSubscriptions(node);
component.subscriptions = {
toggled: oldTree ? oldTree.subscriptions.toggled : true,
children: [],
};
rawSubscriptions.forEach((rawSubscription) => {
let subscription = {
target: {
name: this.targetName(rawSubscription.target, node),
contentType:
typeof rawSubscription.target === "object"
? Array.isArray(rawSubscription.target)
? "array"
: "object"
: rawSubscription.target,
depth: 0,
path: [
...path,
{ type: "item", value: "subscriptions" },
{ type: "item", value: rawSubscription.index },
{ type: "item", value: "target" },
],
toggled: false,
objectType: "subscription",
},
};
if (
oldTree &&
oldTree.subscriptions.children[rawSubscription.index] &&
oldTree.subscriptions.children[rawSubscription.index].target.toggled
) {
subscription.target.toggled = true;
}
if (rawSubscription.target == null) {
if (subscription.target.contentType === "undefined") {
subscription.target.content = "undefined";
} else {
subscription.target.content = "null";
}
subscription.target.hasChildren = false;
} else {
subscription.target.content = this.serializer.serializeContent(
rawSubscription.target,
subscription.target.contentType
);
subscription.target.hasChildren =
subscription.target.contentType === "object"
? Object.keys(rawSubscription.target).length > 0
: subscription.target.contentType === "array"
? rawSubscription.target.length > 0
: false;
}
subscription.target.children = [];
if (subscription.target.toggled) {
subscription.target.children = this.loadObjectChildren(
subscription.target.path,
subscription.target.depth,
subscription.target.contentType,
subscription.target.objectType,
oldTree
);
}
this.addHighlightedKeys(subscription.target);
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
loadGetterContent(getter) {
let obj = this.getObjectProperty(getter.path);
if (obj == null) {
if (typeof obj === "undefined") {
getter.content = "undefined";
getter.contentType = "undefined";
} else {
getter.content = "null";
getter.contentType = "object";
}
getter.hasChildren = false;
} else {
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
getter.contentType = "map";
getter.hasChildren = obj.size > 0;
break;
case obj instanceof Set:
getter.contentType = "set";
getter.hasChildren = obj.size > 0;
break;
case obj instanceof Array:
getter.contentType = "array";
getter.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
getter.contentType = "function";
getter.hasChildren = Reflect.ownKeys(obj).length > 0;
break;
case obj instanceof Object:
getter.contentType = "object";
getter.hasChildren = Reflect.ownKeys(obj).length > 0;
break;
default:
getter.contentType = typeof obj;
getter.hasChildren = false;
}
getter.content = this.serializer.serializeContent(obj, getter.contentType);
}
return getter;
}
// Gives the DOM elements which correspond to the given component node
getDOMElementsRecursive(node) {
if (node.hasOwnProperty("bdom")) {
return this.getDOMElementsRecursive(node.bdom);
}
if (node.hasOwnProperty("content")) {
return this.getDOMElementsRecursive(node.content);
}
if (node.hasOwnProperty("el")) {
if (node.el instanceof Element || node.el instanceof Text) {
return [node.el];
}
}
if (node.hasOwnProperty("child")) {
return this.getDOMElementsRecursive(node.child);
}
if (node.hasOwnProperty("children") && node.children.length > 0) {
let elements = [];
for (const child of node.children) {
if (child) {
elements = elements.concat(this.getDOMElementsRecursive(child));
}
}
if (elements.length > 0) {
return elements;
}
}
if (node.hasOwnProperty("parentEl")) {
if (node.parentEl instanceof Element) {
return [node.parentEl];
}
}
return [];
}
// Triggers the highlight effect around the specified component.
highlightComponent(path) {
// Try to highlight the root component of the app if function called on an app
if (path.length === 1) {
path.push("root");
}
let component = this.getComponentNode(path);
if (!component) {
return;
}
const elements = this.getDOMElementsRecursive(component);
this.highlightElements(elements, component.component.constructor.name);
}
// Edit a reactive state property with the provided value of the given component (path) and the subscription path
editObject(path, value, objectType) {
if (value === "undefined") {
value = undefined;
} else {
try {
value = JSON.parse(value);
} catch (e) {
console.warn("Could not evaluate user property\n", e);
return;
}
}
const item = path.pop();
const obj = this.getObjectProperty(path);
const key = item.hasOwnProperty("symbolIndex")
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
: item.value;
if (!obj) {
return;
}
if (objectType === "subscription") {
this.reactive(obj)[key] = value;
} else {
obj[key] = value;
if (objectType === "props" || objectType === "instance") {
const component = this.getComponentNode(path);
if (component.__proto__.hasOwnProperty("render")) {
this.getComponentNode(path).render();
} else {
component.root.render();
}
} else if (objectType === "env") {
[...this.apps][path[0]].root.render(true);
}
}
}
// Recursively checks if the given html element corresponds to a component in the components tree.
// Immediatly returns the path of the first component which matches the element
searchElement(node, path, element) {
if (!node?.bdom) {
return null;
}
const results = this.getDOMElementsRecursive(node);
let hasElementInChildren = false;
for (const result of results) {
if (result.isEqualNode(element)) {
return path;
}
if (result.contains(element)) {
hasElementInChildren = true;
}
}
if (hasElementInChildren) {
for (const [key, child] of Object.entries(node.children)) {
const result = this.searchElement(child, path.concat([key]), element);
if (result) {
return result;
}
}
}
return null;
}
// Returns the path to the component which is currently being inspected
getElementPath(element) {
if (element) {
// Create an array with the html element and all its successive parents
const parentsList = [element];
if (element.tagName !== "BODY") {
while (element && element?.parentElement?.tagName !== "BODY") {
element = element.parentElement;
parentsList.push(element);
}
}
const appsArray = [...this.apps];
// Try to find a correspondance between the elements in the array and the owl component, stops at first result found
for (const elem of parentsList) {
for (const [index, app] of appsArray.entries()) {
const inspectedPath = this.searchElement(app.root, ["root"], elem);
if (inspectedPath) {
inspectedPath.unshift(index.toString());
return inspectedPath;
}
}
}
}
// If nothing was found, return the path of the first root component found in the apps
const appIndex = [...this.apps].findIndex((app) => app.root);
return [appIndex.toString(), "root"];
}
// Returns the tree of components of the inspected page in a parsed format
// Use inspectedPath to specify the path of the selected component
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
const appsArray = [...this.apps];
if (inspectedPath && !this.getComponentNode(inspectedPath)) {
inspectedPath = null;
}
const trees = appsArray.map((app, index) => {
let oldTree;
if (oldTrees) {
oldTree = oldTrees[index];
}
let appNode = {};
appNode = {
name: app?.name ? `App (${app.name})` : "App " + (index + 1),
path: [index.toString()],
key: "",
depth: 0,
toggled: true,
selected: false,
highlighted: false,
version: app.__proto__.constructor?.version
? app.__proto__.constructor.version
: "<2.0.8",
children: [],
};
if (app.root) {
const root = {
name: app.root.component.constructor.name,
path: [index.toString(), "root"],
key: "",
depth: 1,
toggled: true,
selected: false,
highlighted: false,
};
if (oldTree) {
appNode.toggled = oldTree.toggled;
oldTree = oldTree.children[0];
}
if (oldTree) {
root.toggled = oldTree.toggled;
}
// If no path is provided, it defaults to the target of the inspect element action
if (!inspectedPath) {
inspectedPath = this.getElementPath($0);
}
if (inspectedPath.join("/") === index.toString()) {
appNode.selected = true;
root.highlighted = true;
} else if (inspectedPath.join("/") === index.toString() + "/root") {
root.selected = true;
}
root.children = this.fillTree(app.root, root, inspectedPath.join("/"), oldTree);
appNode.children.push(root);
}
return appNode;
});
const component = this.getComponentDetails(inspectedPath, oldDetails);
return trees ? [trees, component] : [];
}
// Recursively fills the components tree as a parsed version
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
const children = [];
for (const [key, appChild] of Object.entries(appNode.children)) {
let child = {
name: appChild.component.constructor.name,
key: key,
depth: treeNode.depth + 1,
toggled: true,
selected: false,
highlighted: false,
};
child.path = treeNode.path.concat([child.key]);
let oldChild = null;
if (oldBranch) {
const searchResult = oldBranch.children.find((o) => o.key === key);
if (searchResult) {
oldChild = searchResult;
child.toggled = oldChild.toggled;
}
}
const childPathString = child.path.join("/");
if (childPathString === inspectedPathString) {
child.selected = true;
} else if (childPathString.includes(inspectedPathString)) {
child.highlighted = true;
}
child.children = this.fillTree(appChild, child, inspectedPathString, oldChild);
children.push(child);
}
return children;
}
getObservedVariables(current) {
const res = [...current];
for (let i = 0; i < current.length; i++) {
const path = current[i].path;
const parent = this.getObjectProperty(path.slice(0, path.length - 1));
if (parent && !(typeof parent === "string" && parent.startsWith("Exception: "))) {
const result = this.serializeObjectChild(
parent,
path.at(-1),
0,
"observed",
path.slice(0, path.length - 1),
{},
{}
);
result.hasChildren = false;
result.visible = true;
const index = path.findIndex((key) => typeof key !== "string");
if (index > 1) {
const componentNode = this.getComponentNode(path.slice(0, index));
result.path = [this.getComponentSimplifiedPath(componentNode)].concat(
path.slice(index)
);
}
res[i] = result;
} else {
res[i].visible = false;
}
}
return res;
}
// Returns the path of the given component node
getComponentPath(componentNode) {
let path = [];
if (componentNode.parentKey) {
path = [componentNode.parentKey];
while (componentNode.parent && componentNode.parent.parentKey) {
componentNode = componentNode.parent;
path.unshift(componentNode.parentKey);
}
}
path.unshift("root");
const appsArray = [...this.apps];
let index = appsArray.findIndex((app) => app === componentNode.app);
path.unshift(index.toString());
return path;
}
// Returns the simplified path of the given component node (using component names and indexes)
getComponentSimplifiedPath(componentNode) {
let path = componentNode.name;
if (componentNode.parentKey) {
while (componentNode.parent) {
const previousKey = componentNode.parentKey;
componentNode = componentNode.parent;
path = `${componentNode.name}/${Reflect.ownKeys(componentNode.children).indexOf(
previousKey
)}/${path}`;
}
}
const appsArray = [...this.apps];
let index = appsArray.findIndex((app) => app === componentNode.app);
path = index.toString() + (path.length ? `/${path}` : "");
return path;
}
// Store the object into a temp window variable and log it to the console
sendObjectToConsole(path) {
const obj = this.getObjectProperty(path);
let index = 1;
while (window["temp" + index] !== undefined) {
index++;
}
window["temp" + index] = obj;
console.log("temp" + index + " = ", window["temp" + index]);
}
// Inspect the DOM of the component in the elements tab of the devtools
inspectComponentDOM(path) {
const componentNode = this.getComponentNode(path);
const elements = this.getDOMElementsRecursive(componentNode);
if (IS_FIREFOX) {
window.$temp = elements[0];
} else {
inspect(elements[0]);
}
}
// Inspect the DOM of the component in the elements tab of the devtools
inspectComponentCompiledTemplate(path) {
const componentNode = this.getComponentNode(path);
const template = componentNode.component.constructor.template;
if (IS_FIREFOX) {
window.$temp = componentNode.app.templates[template];
} else {
inspect(componentNode.app.templates[template]);
}
}
// Inspect source code of the component (corresponds to inspecting its constructor)
inspectComponentRawTemplate(path) {
const componentNode = this.getComponentNode(path);
const template = componentNode.component.constructor.template;
const templateNode = componentNode.app.rawTemplates[template];
console.log(templateNode);
}
// Inspect source code of the function given by its path
inspectFunctionSource(path) {
const obj = this.getObjectProperty(path);
if (IS_FIREFOX) {
window.$temp = obj;
} else {
inspect(obj);
}
}
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;
for (const [key, value] of Object.entries(component)) {
if (target === this.toRaw(value)) {
return key;
}
}
// check on props
for (const [key, value] of Object.entries(component.props)) {
if (target === this.toRaw(value)) {
return `props.${key}`;
}
}
return "[unknown]";
}
/**
* Removes subscriptions that are a direct child of another subscription:
* they will be reachable from the top level by expanding observed keys.
*
* @param {ComponentNode} node
* @returns {{ keys: PropertyKey[], target: unknown}[]} the top level
* subscriptions of the node
*/
topLevelSubscriptions(node) {
const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index }));
const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o)));
const toOmit = new Set(
subscriptions
.flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k])))
.filter((obj) => !topLevelValues.has(obj))
);
return subscriptions.filter(({ target }) => !toOmit.has(target));
}
addHighlightedKeys(child) {
const { path } = child;
const subscriptionIndex = path.findIndex((item) => typeof item !== "string");
if (path[subscriptionIndex]?.value === "subscriptions") {
const node = this.getComponentNode(path.slice(0, subscriptionIndex));
// Add observed keys
const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys]));
const target = this.getObjectProperty(child.path);
child.keys = targetToKeys.get(target)?.map((k) => String(k));
}
}
}
function compareKeys(a, b) {
const isSymbolA = typeof a === "symbol";
const isSymbolB = typeof b === "symbol";
if (isSymbolA && !isSymbolB) {
return 1; // Place Symbols at the end
} else if (!isSymbolA && isSymbolB) {
return -1; // Place non-Symbols at the beginning
} else {
return String(a).localeCompare(String(b)); // Sort other keys alphabetically
}
}
function checkOwlStatus() {
let owlStatus = 2;
if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
if (window.__OWL_DEVTOOLS__ || window.owl?.App) {
owlStatus = 1;
} else {
// It seems that sending a 0 results in undefined at the other end for some reason
owlStatus = -1;
}
}
window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus });
}
if (!window.__OWL_DEVTOOLS__) {
let val;
const descriptor = Object.getOwnPropertyDescriptor(window, "__OWL_DEVTOOLS__") || {
get() {
return val;
},
set(value) {
val = value;
},
};
Object.defineProperty(window, "__OWL_DEVTOOLS__", {
get() {
return descriptor.get.call(this);
},
set(value) {
descriptor.set.call(this, value);
if (value?.Fiber !== undefined) {
window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
}
window.top.postMessage({ source: "owl-devtools", type: "FrameReady" });
checkOwlStatus();
},
});
// Do note that the reload message is not sent on the top window so that it is not intercepted when originating
// from an iframe
window.postMessage({ source: "owl-devtools", type: "Reload" });
} else if (
window.__OWL_DEVTOOLS__?.Fiber !== undefined &&
!window.__OWL__DEVTOOLS_GLOBAL_HOOK__
) {
window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
window.postMessage({ source: "owl-devtools", type: "Reload" });
}
// Listener that checks whether owl is available on the page and if it has the right version
window.addEventListener(
"message",
function (event) {
if (event.data.source === "owl-devtools-background" && event.data.type === "checkOwlStatus") {
checkOwlStatus();
}
},
false
);
checkOwlStatus();
// Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes)
return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;
})();