Compare commits

..

1 Commits

Author SHA1 Message Date
Samuel Degueldre e1ff7ac6a4 [FIX] runtime: do not crash when capturing context with getter
When attempting to capture a rendering context that contains an
enumerable getter, there is a crash because we attempt to write a value
on the getter. This commit fixes that by manually climbing the prototype
chain to copy the values instead, and ignoring getters.
2023-06-09 07:34:54 +02:00
21 changed files with 382 additions and 527 deletions
+18 -50
View File
@@ -175,21 +175,11 @@ function createAttrUpdater(attr) {
} }
function attrsSetter(attrs) { function attrsSetter(attrs) {
if (isArray(attrs)) { if (isArray(attrs)) {
if (attrs[0] === "class") { setAttribute.call(this, attrs[0], attrs[1]);
setClass.call(this, attrs[1]);
}
else {
setAttribute.call(this, attrs[0], attrs[1]);
}
} }
else { else {
for (let k in attrs) { for (let k in attrs) {
if (k === "class") { setAttribute.call(this, k, attrs[k]);
setClass.call(this, attrs[k]);
}
else {
setAttribute.call(this, k, attrs[k]);
}
} }
} }
} }
@@ -201,12 +191,7 @@ function attrsUpdater(attrs, oldAttrs) {
if (val === oldAttrs[1]) { if (val === oldAttrs[1]) {
return; return;
} }
if (name === "class") { setAttribute.call(this, name, val);
updateClass.call(this, val, oldAttrs[1]);
}
else {
setAttribute.call(this, name, val);
}
} }
else { else {
removeAttribute.call(this, oldAttrs[0]); removeAttribute.call(this, oldAttrs[0]);
@@ -216,23 +201,13 @@ function attrsUpdater(attrs, oldAttrs) {
else { else {
for (let k in oldAttrs) { for (let k in oldAttrs) {
if (!(k in attrs)) { if (!(k in attrs)) {
if (k === "class") { removeAttribute.call(this, k);
updateClass.call(this, "", oldAttrs[k]);
}
else {
removeAttribute.call(this, k);
}
} }
} }
for (let k in attrs) { for (let k in attrs) {
const val = attrs[k]; const val = attrs[k];
if (val !== oldAttrs[k]) { if (val !== oldAttrs[k]) {
if (k === "class") { setAttribute.call(this, k, val);
updateClass.call(this, val, oldAttrs[k]);
}
else {
setAttribute.call(this, k, val);
}
} }
} }
} }
@@ -3900,10 +3875,6 @@ class CodeGenerator {
}) })
.join(""); .join("");
} }
translate(str) {
const match = translationRE.exec(str);
return match[1] + this.translateFn(match[2]) + match[3];
}
/** /**
* @returns the newly created block name, if any * @returns the newly created block name, if any
*/ */
@@ -3981,7 +3952,8 @@ class CodeGenerator {
let { block, forceNewBlock } = ctx; let { block, forceNewBlock } = ctx;
let value = ast.value; let value = ast.value;
if (value && ctx.translate !== false) { if (value && ctx.translate !== false) {
value = this.translate(value); const match = translationRE.exec(value);
value = match[1] + this.translateFn(match[2]) + match[3];
} }
if (!ctx.inPreTag) { if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " "); value = value.replace(whitespaceRE, " ");
@@ -4522,12 +4494,11 @@ class CodeGenerator {
else { else {
let value; let value;
if (ast.defaultValue) { if (ast.defaultValue) {
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
if (ast.value) { if (ast.value) {
value = `withDefault(${expr}, \`${defaultValue}\`)`; value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
} }
else { else {
value = `\`${defaultValue}\``; value = `\`${ast.defaultValue}\``;
} }
} }
else { else {
@@ -4908,10 +4879,10 @@ function parseDOMNode(node, ctx) {
let model = null; let model = null;
for (let attr of nodeAttrsNames) { for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr); const value = node.getAttribute(attr);
if (attr === "t-on" || attr === "t-on-") { if (attr.startsWith("t-on")) {
throw new OwlError("Missing event name with t-on directive"); if (attr === "t-on") {
} throw new OwlError("Missing event name with t-on directive");
if (attr.startsWith("t-on-")) { }
on = on || {}; on = on || {};
on[attr.slice(5)] = value; on[attr.slice(5)] = value;
} }
@@ -5535,7 +5506,7 @@ function compile(template, options = {}) {
} }
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
const version = "2.1.3"; const version = "2.1.2";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -5614,8 +5585,6 @@ window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
apps: new Set(), apps: new Set(),
Fiber: Fiber, Fiber: Fiber,
RootFiber: RootFiber, RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
}); });
class App extends TemplateSet { class App extends TemplateSet {
constructor(Root, config = {}) { constructor(Root, config = {}) {
@@ -5868,9 +5837,8 @@ function useChildSubEnv(envExtension) {
* will run a cleanup function before patching and before unmounting the * will run a cleanup function before patching and before unmounting the
* the component. * the component.
* *
* @template T * @param {Effect} effect the effect to run on component mount and/or patch
* @param {Effect<T>} effect the effect to run on component mount and/or patch * @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and * dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run * run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because * again. The default value returns an array containing only NaN because
@@ -5952,6 +5920,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml }; export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2023-06-28T09:17:13.630Z'; __info__.date = '2023-04-29T07:45:54.333Z';
__info__.hash = '432ff44'; __info__.hash = 'aabb755';
__info__.url = 'https://github.com/odoo/owl'; __info__.url = 'https://github.com/odoo/owl';
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.1.4", "version": "2.1.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -5594,9 +5594,9 @@
} }
}, },
"tough-cookie": { "tough-cookie": {
"version": "4.1.3", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"psl": "^1.1.33", "psl": "^1.1.33",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.1.4", "version": "2.1.3",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"module": "dist/owl.es.js", "module": "dist/owl.es.js",
+5 -25
View File
@@ -36,18 +36,10 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
export function attrsSetter(this: HTMLElement, attrs: any) { export function attrsSetter(this: HTMLElement, attrs: any) {
if (isArray(attrs)) { if (isArray(attrs)) {
if (attrs[0] === "class") { setAttribute.call(this, attrs[0], attrs[1]);
setClass.call(this, attrs[1]);
} else {
setAttribute.call(this, attrs[0], attrs[1]);
}
} else { } else {
for (let k in attrs) { for (let k in attrs) {
if (k === "class") { setAttribute.call(this, k, attrs[k]);
setClass.call(this, attrs[k]);
} else {
setAttribute.call(this, k, attrs[k]);
}
} }
} }
} }
@@ -60,11 +52,7 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
if (val === oldAttrs[1]) { if (val === oldAttrs[1]) {
return; return;
} }
if (name === "class") { setAttribute.call(this, name, val);
updateClass.call(this, val, oldAttrs[1]);
} else {
setAttribute.call(this, name, val);
}
} else { } else {
removeAttribute.call(this, oldAttrs[0]); removeAttribute.call(this, oldAttrs[0]);
setAttribute.call(this, name, val); setAttribute.call(this, name, val);
@@ -72,21 +60,13 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
} else { } else {
for (let k in oldAttrs) { for (let k in oldAttrs) {
if (!(k in attrs)) { if (!(k in attrs)) {
if (k === "class") { removeAttribute.call(this, k);
updateClass.call(this, "", oldAttrs[k]);
} else {
removeAttribute.call(this, k);
}
} }
} }
for (let k in attrs) { for (let k in attrs) {
const val = attrs[k]; const val = attrs[k];
if (val !== oldAttrs[k]) { if (val !== oldAttrs[k]) {
if (k === "class") { setAttribute.call(this, k, val);
updateClass.call(this, val, oldAttrs[k]);
} else {
setAttribute.call(this, k, val);
}
} }
} }
} }
+14 -2
View File
@@ -8,6 +8,12 @@ import { OwlError } from "./error_handling";
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
const ObjectCreate = Object.create; const ObjectCreate = Object.create;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const ObjectDefineProperty = Object.defineProperty;
const ObjectEntries = Object.entries;
const hasOwnProperty = (obj: Object, prop: PropertyKey) =>
Object.prototype.hasOwnProperty.call(obj, prop);
/** /**
* This file contains utility functions that will be injected in each template, * This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code. * to perform various useful tasks in the compiled code.
@@ -49,8 +55,14 @@ function callSlot(
function capture(ctx: any): any { function capture(ctx: any): any {
const result = ObjectCreate(ctx); const result = ObjectCreate(ctx);
for (let k in ctx) { let current = ctx;
result[k] = ctx[k]; while (current && current !== Object.prototype) {
for (const [key, descriptor] of ObjectEntries(ObjectGetOwnPropertyDescriptors(current))) {
if (!hasOwnProperty(result, key) && "value" in descriptor) {
ObjectDefineProperty(result, key, descriptor);
}
}
current = ObjectGetPrototypeOf(current);
} }
return result; return result;
} }
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
export const version = "2.1.4"; export const version = "2.1.3";
-31
View File
@@ -145,34 +145,3 @@ test("class attribute (with a preexisting value", async () => {
patch(tree, block([""])); patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`); expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
}); });
test("block-class attributes with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([{ class: "eagle" }]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([{ class: "falcon" }]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([{}]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
});
test("block-class attributes (array syntax) with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([["class", "eagle"]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([["class", "falcon"]]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([["class", ""]]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
patch(tree, block([["class", "buzzard"]]));
expect(fixture.innerHTML).toBe(`<div class="owl buzzard"></div>`);
});
@@ -707,123 +707,6 @@ exports[`attributes updating classes (with obj notation) 1`] = `
}" }"
`; `;
exports[`attributes various combinations of class, t-att-class, and t-att 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\" block-attributes=\\"1\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
let attr2 = {class:'a'};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 7`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = ('b');
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 8`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various escapes 1`] = ` exports[`attributes various escapes 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
-29
View File
@@ -371,33 +371,4 @@ describe("attributes", () => {
// not sure about this. maybe we want to remove the attribute? // not sure about this. maybe we want to remove the attribute?
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>'); expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
}); });
test("various combinations of class, t-att-class, and t-att", () => {
const template1 = `<div t-att="{ class: 'a' }" class="c">content</div>`;
expect(renderToString(template1)).toBe('<div class="c a">content</div>');
const template2 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}" class="c">content</div>`;
expect(renderToString(template2)).toBe('<div class="c a b">content</div>');
const template3 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template3)).toBe('<div class="c a b">content</div>');
const template4 = `<div class="c" t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template4)).toBe('<div class="c a b">content</div>');
const template5 = `<div class="c" t-att-class="{'b': true}" t-att="{ class: 'a' }">content</div>`;
expect(renderToString(template5)).toBe('<div class="c b a">content</div>');
const template6 = `<div class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template6)).toBe('<div class="c b">content</div>');
const template7 = `<div class="c" t-attf-class="{{'b'}}">content</div>`;
expect(renderToString(template7)).toBe('<div class="c b">content</div>');
const template8 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template8)).toBe('<div class="c a b">content</div>');
const template9 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template9)).toBe('<div class="a b">content</div>');
});
}); });
@@ -1,5 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-call component with an enumerable getter, t-call inside slot 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const callTemplate_1 = app.getTemplate(\`sub\`);
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
function slot1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['foo'];
return block1([txt1]);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {});
}
}"
`;
exports[`t-call dynamic t-call 1`] = ` exports[`t-call dynamic t-call 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+22
View File
@@ -425,4 +425,26 @@ describe("t-call", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("Bchild"); expect(fixture.innerHTML).toBe("Bchild");
}); });
test("component with an enumerable getter, t-call inside slot", async () => {
class Child extends Component {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child><t t-call="sub"/></Child>`;
}
// simulate adding a getter with patch in odoo: getter will be enumarable
Object.defineProperty(Parent.prototype, "foo", {
get() {
return 1;
},
enumerable: true,
});
const app = new App(Parent);
app.addTemplate("sub", `<div t-esc="foo"/>`);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
});
}); });
@@ -1,10 +1,11 @@
const { Component, useRef, useEffect } = owl; const { Component, useRef, useEffect } = owl;
import { useStore } from "../../../store/store"; import { useStore } from "../../../store/store";
import { ObjectTreeElement } from "./object_tree_element/object_tree_element"; import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
import { Subscriptions } from "./subscriptions/subscriptions";
export class DetailsWindow extends Component { export class DetailsWindow extends Component {
static template = "devtools.DetailsWindow"; static template = "devtools.DetailsWindow";
static components = { ObjectTreeElement }; static components = { ObjectTreeElement, Subscriptions };
setup() { setup() {
this.store = useStore(); this.store = useStore();
this.contextMenu = useRef("contextmenu"); this.contextMenu = useRef("contextmenu");
@@ -53,11 +53,7 @@
</div> </div>
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i> <i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
</div> </div>
<div t-if="store.activeComponent.subscriptions.toggled" id="subscriptionsPanel"> <Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
<ObjectTreeElement object="subscription.target"/>
</t>
</div>
</div> </div>
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1"> <div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
<div class="d-flex mb-2"> <div class="d-flex mb-2">
@@ -41,22 +41,23 @@ export class ObjectTreeElement extends Component {
return JSON.stringify(this.props.object.path); return JSON.stringify(this.props.object.path);
} }
get keyChanges() { get objectName() {
return this.props.object.keys?.includes("Symbol(Key changes)"); return this.props.object.name;
} }
classFor(object) { get objectLineClass() {
// Prototype items will be dyed down to appear less important // Prototype items will be dyed down to appear less important
if (object.path.some((item) => item?.type === "prototype") && !object.keepLit) { if (this.pathAsString.includes('{"type":"prototype",')) {
return "attenuate"; return { attenuate: true };
} }
// Same for subscription items which are not present in the keys while the keys will be bold // Same for subscription items which are not present in the keys while the keys will be bold
if (object.objectType === "subscription" && object.depth > 0) { if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
if (this.props.object.keys?.includes(object.name.toString())) { if (this.props.keys.includes(this.props.object.name.toString())) {
return "fw-bolder"; return { "fw-bolder": true };
} }
return "attenuate"; return { attenuate: true };
} }
return {};
} }
get objectPadding() { get objectPadding() {
@@ -2,7 +2,7 @@
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="devtools.ObjectTreeElement" owl="1"> <t t-name="devtools.ObjectTreeElement" owl="1">
<div class="m-0 p-0 text-nowrap w-100 object-line" <div class="m-0 p-0 text-nowrap w-100 object-line"
t-att-class="props.class" t-att-class="objectLineClass"
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)" t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
t-on-contextmenu.prevent="openMenu" t-on-contextmenu.prevent="openMenu"
> >
@@ -11,7 +11,7 @@
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}" t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};" t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
/> />
<t t-esc="props.object.name"/> <t t-esc="objectName"/>
<t t-if="props.object.content.length > 0">: </t> <t t-if="props.object.content.length > 0">: </t>
<t t-if="props.object.contentType == 'getter'"> <t t-if="props.object.contentType == 'getter'">
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)"> <span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
@@ -28,7 +28,6 @@
</t> </t>
</span> </span>
</t> </t>
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
</div> </div>
</div> </div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu"> <div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
@@ -41,7 +40,8 @@
</div> </div>
<t t-if="props.object.toggled" t-key="contextMenuId"> <t t-if="props.object.toggled" t-key="contextMenuId">
<t t-foreach="props.object.children" t-as="child" t-key="child_index"> <t t-foreach="props.object.children" t-as="child" t-key="child_index">
<ObjectTreeElement object="child" class="this.classFor(child)"/> <ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
<ObjectTreeElement t-else="" object="child"/>
</t> </t>
</t> </t>
</t> </t>
@@ -0,0 +1,30 @@
const { Component } = owl;
import { useStore } from "../../../../store/store";
import { ObjectTreeElement } from "../object_tree_element/object_tree_element";
export class Subscriptions extends Component {
static template = "devtools.Subscriptions";
static components = { ObjectTreeElement };
setup() {
this.store = useStore();
}
// Used to display the keys in a compact way
keysContent(index) {
const keys = this.store.activeComponent.subscriptions.children[index].keys;
let content = JSON.stringify(keys);
const maxLength = 50;
content = content.replace(/,/g, ", ");
if (content.length > maxLength) {
content = content.slice(0, content.lastIndexOf(",", maxLength - 5)) + ", ...]";
}
return content;
}
expandKeys(event, index) {
this.store.activeComponent.subscriptions.children[index].keysExpanded =
!this.store.activeComponent.subscriptions.children[index].keysExpanded;
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="devtools.Subscriptions" owl="1">
<div id="subscriptionsPanel">
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
<div class="my-2">
<div class="my-0 p-0 object-line" t-on-click.stop="(ev) => this.expandKeys(ev, subscription_index)">
<span class="ps-1 text-nowrap">
<i class="fa fa-caret-right ms-1" t-attf-style="cursor: pointer;{{subscription.keysExpanded ? 'transform: rotate(90deg);' : ''}}"></i>
keys: <span class="key-name"><t t-esc="this.keysContent(subscription_index)"/></span>
</span>
</div>
<div t-foreach="subscription.keys" t-as="key" t-key="key_index" class="my-0 p-0 object-line" t-attf-style="display: {{subscription.keysExpanded ? 'flex' : 'none'}}">
<div style="transform: translateX(calc(1.1rem))" class="key-content">
<i class="fa fa-caret-right mx-1" t-attf-style="cursor: pointer; visibility: hidden;"></i>
<t t-esc="key"/>
</div>
</div>
<ObjectTreeElement object="subscription.target" keys="subscription.keys"/>
</div>
</t>
</div>
</t>
</templates>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="devtools.Event" owl="1"> <t t-name="devtools.Event" owl="1">
<div class="event-container" t-att-class="{ 'event-last': props.event.isLast }"> <div class="event-container">
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay"> <div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
<div class="ps-2 text-nowrap"> <div class="ps-2 text-nowrap">
<i class="fa px-1 pointer-icon caret" <i class="fa px-1 pointer-icon caret"
+24 -46
View File
@@ -102,19 +102,21 @@ export const store = reactive({
if (IS_FIREFOX) { if (IS_FIREFOX) {
await evalInWindow("window.$0 = $0;", this.activeFrame); await evalInWindow("window.$0 = $0;", this.activeFrame);
} }
const [apps, details] = await evalFunctionInWindow( const apps = await evalFunctionInWindow(
"getComponentsTree", "getComponentsTree",
fromOld && this.activeComponent fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
? [this.activeComponent.path, this.apps, this.activeComponent]
: [],
this.activeFrame this.activeFrame
); );
this.apps = apps ? apps : []; this.apps = apps ? apps : [];
if (!fromOld && this.settings.expandByDefault) { if (!fromOld && this.settings.expandByDefault) {
this.apps.forEach((tree) => expandNodes(tree, true)); this.apps.forEach((tree) => expandNodes(tree, true));
} }
keepEnvLit(details); const component = await evalFunctionInWindow(
this.activeComponent = details; "getComponentDetails",
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
this.activeFrame
);
this.activeComponent = component;
}, },
// Select a component by retrieving its details from the page based on its path // Select a component by retrieving its details from the page based on its path
@@ -151,11 +153,9 @@ export const store = reactive({
[component.path], [component.path],
this.activeFrame this.activeFrame
); );
if (!details) { this.activeComponent = details;
if (!this.activeComponent) {
await this.loadComponentsTree(false); await this.loadComponentsTree(false);
} else {
keepEnvLit(details);
this.activeComponent = details;
} }
if (this.page !== "ComponentsTab") { if (this.page !== "ComponentsTab") {
this.switchTab("ComponentsTab"); this.switchTab("ComponentsTab");
@@ -413,7 +413,12 @@ export const store = reactive({
if (!scriptsLoaded) { if (!scriptsLoaded) {
await loadScripts(frame); await loadScripts(frame);
} }
evalFunctionInWindow("initDevtools", [frame], frame); evalInWindow(
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
store.devtoolsId
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
frame
);
if (!this.frameUrls.includes(frame)) { if (!this.frameUrls.includes(frame)) {
this.frameUrls = [...this.frameUrls, frame]; this.frameUrls = [...this.frameUrls, frame];
} }
@@ -633,7 +638,7 @@ init();
async function init() { async function init() {
store.devtoolsId = await getTabURL(); store.devtoolsId = await getTabURL();
evalFunctionInWindow("initDevtools", []); evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
await loadSettings(); await loadSettings();
@@ -666,7 +671,7 @@ async function init() {
}, 500); }, 500);
} }
let rootRendersTimeout = false; let flushRendersTimeout = false;
// Connect to the port to communicate to the background script // Connect to the port to communicate to the background script
browserInstance.runtime.onConnect.addListener((port) => { browserInstance.runtime.onConnect.addListener((port) => {
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) { if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
@@ -675,7 +680,7 @@ browserInstance.runtime.onConnect.addListener((port) => {
if (msg.type === "Reload") { if (msg.type === "Reload") {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;"); store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) { if (store.owlStatus) {
evalFunctionInWindow("initDevtools", []); evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
await store.resetData(); await store.resetData();
} }
} }
@@ -689,9 +694,9 @@ browserInstance.runtime.onConnect.addListener((port) => {
if (msg.type === "RefreshApps") { if (msg.type === "RefreshApps") {
store.loadComponentsTree(true); store.loadComponentsTree(true);
} }
// When message of type Complete is received, overwrite the component tree with the new one from page // When message of type Flush is received, overwrite the component tree with the new one from page
// A Complete message is sent everytime a root render is triggered on the page // A flush message is sent everytime a component is rendered on the page
if (msg.type === "Complete") { if (msg.type === "Flush") {
if (msg.origin.frame !== store.activeFrame) { if (msg.origin.frame !== store.activeFrame) {
return; return;
} }
@@ -700,8 +705,8 @@ browserInstance.runtime.onConnect.addListener((port) => {
} }
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered // This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data)); store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(rootRendersTimeout); clearTimeout(flushRendersTimeout);
rootRendersTimeout = setTimeout(() => { flushRendersTimeout = setTimeout(() => {
store.renderPaths.clear(); store.renderPaths.clear();
}, 100); }, 100);
store.loadComponentsTree(true); store.loadComponentsTree(true);
@@ -782,7 +787,6 @@ function loadEvents(events) {
} }
event.origin = null; event.origin = null;
event.toggled = false; event.toggled = false;
event.isLast = false;
// Logic to retrace the origin of the event if it is not a root render event // Logic to retrace the origin of the event if it is not a root render event
if (!event.type.includes("render")) { if (!event.type.includes("render")) {
for (let i = store.events.length - 1; i >= 0; i--) { for (let i = store.events.length - 1; i >= 0; i--) {
@@ -832,7 +836,6 @@ function loadEvents(events) {
// Make sure we add the event while keeping the whole list ordered by id // Make sure we add the event while keeping the whole list ordered by id
addEventSorted(event); addEventSorted(event);
} }
store.events[store.events.length - 1].isLast = true;
} }
// Deselect component and remove highlight on all children // Deselect component and remove highlight on all children
@@ -890,31 +893,6 @@ function expandNodes(node, blacklist = false) {
} }
} }
// This function transforms the env part of the details such that all env keys are not
// greyed out in the UI at their first occurence
function keepEnvLit(details) {
let alreadyMet = new Set();
for (let i = 0; i < details.env.children.length; i++) {
if (i < details.env.children.length - 1) {
alreadyMet.add(details.env.children[i].name);
} else {
let lastElement = details.env.children[i];
while (lastElement.children.at(-1).name === "[[Prototype]]") {
for (const [index, child] of lastElement.children.entries()) {
if (index < lastElement.children.length - 1) {
if (!alreadyMet.has(child.name)) {
child.keepLit = true;
alreadyMet.add(child.name);
}
} else {
lastElement = child;
}
}
}
}
}
}
// Fold the node given in entry and all of its children // Fold the node given in entry and all of its children
function foldNodes(node) { function foldNodes(node) {
node.toggled = false; node.toggled = false;
-8
View File
@@ -126,10 +126,6 @@
color: var(--prototype-color); color: var(--prototype-color);
} }
.key-changes {
background-color: var(--version-bg);
}
.event-container { .event-container {
border-bottom: 1px solid rgb(240, 238, 238); border-bottom: 1px solid rgb(240, 238, 238);
padding-top: 2px!important; padding-top: 2px!important;
@@ -137,10 +133,6 @@
font-size: 11px; font-size: 11px;
} }
.event-last {
border-bottom: 3px solid rgb(225, 154, 0);
}
.getter-content:hover { .getter-content:hover {
text-decoration: underline; text-decoration: underline;
} }
@@ -15,6 +15,8 @@
// in __OWL_DEVTOOLS__ // in __OWL_DEVTOOLS__
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw; this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive; this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
// Set to keep track of the fibers that are in the flush queue
this.queuedFibers = new WeakSet();
// Set to keep track of the HTML elements we added to the page // Set to keep track of the HTML elements we added to the page
this.addedElements = []; this.addedElements = [];
// To keep track of the succession order of the render events // To keep track of the succession order of the render events
@@ -22,6 +24,7 @@
// Set to keep track of the frame on which this script is loaded // Set to keep track of the frame on which this script is loaded
this.frame = "top"; this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page // Allows to launch a message each time an iframe html element is added to the page
const self = this;
const iFrameObserver = new MutationObserver(function (mutationsList) { const iFrameObserver = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) { mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) { mutation.addedNodes.forEach(function (addedNode) {
@@ -44,7 +47,12 @@
}); });
}); });
iFrameObserver.observe(document.body, { subtree: true, childList: true }); iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods(); this.patchAppsSetMethods();
if (this.apps.size > 0) {
this.patchAppMethods();
}
this.recordEvents = false; this.recordEvents = false;
this.traceRenderings = false; this.traceRenderings = false;
this.traceSubscriptions = false; this.traceSubscriptions = false;
@@ -117,15 +125,6 @@
length += element.length; length += element.length;
result.push(element); 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);
}
return "{" + result.join(", ") + "}"; return "{" + result.join(", ") + "}";
}, },
map(obj) { map(obj) {
@@ -173,40 +172,34 @@
}; };
} }
initDevtools(frame = "top") {
if (!this.devtoolsInit) {
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. // Modify the methods of the apps set in order to send a message each time it is modified.
patchAppsSetMethods() { patchAppsSetMethods() {
const originalAdd = this.apps.add; const originalAdd = this.apps.add;
const originalDelete = this.apps.delete; const originalDelete = this.apps.delete;
const self = this;
this.apps.add = function () { this.apps.add = function () {
originalAdd.call(this, ...arguments); originalAdd.call(this, ...arguments);
if (!self.destroyPatched) {
const newApp = arguments[0];
// It is not a given that apps have a root node at creation so we need to wait
if (newApp.root) {
self.patchDestroyMethod(newApp.root);
} else {
let root = null;
Object.defineProperty(newApp, "root", {
get() {
return root;
},
set(value) {
root = value;
if (!self.destroyPatched) {
self.patchDestroyMethod(root);
}
},
});
}
}
self.patchAppMethods();
window.top.postMessage({ window.top.postMessage({
source: "owl-devtools", source: "owl-devtools",
type: "RefreshApps", type: "RefreshApps",
@@ -221,24 +214,72 @@
}; };
} }
patchDestroyMethod(root) {
if (!this.destroyPatched) {
// Signals when a component is destroyed
const originalDestroy = root.constructor.prototype._destroy;
const self = this;
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);
}
};
this.destroyPatched = true;
}
}
// Modify methods of each app so that it triggers messages on each flush and component render // Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() { patchAppMethods() {
let app; if (this.appsPatched) {
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
if (!app.root) {
return; return;
} }
let app = this.apps.values().next().value;
const self = this; const self = this;
if (app.root) {
this.patchDestroyMethod(app.root);
} else {
const originalMount = app.constructor.prototype.mount;
app.constructor.prototype.mount = async function (...args) {
const result = await originalMount.call(this, ...args);
const root = this.root;
self.patchDestroyMethod(root);
app.constructor.prototype.mount = originalMount;
return result;
};
}
const originalFlush = app.scheduler.constructor.prototype.flush; const originalFlush = app.scheduler.constructor.prototype.flush;
let inFlush = false; let inFlush = false;
let _render = false; let _render = false;
app.scheduler.constructor.prototype.flush = function () { app.scheduler.constructor.prototype.flush = function () {
// Used to know when a render is triggered inside the flush method or not // Used to know when a render is triggered inside the flush method or not
inFlush = true; inFlush = true;
[...this.tasks].map((fiber) => {
if (fiber.counter === 0 && !self.queuedFibers.has(fiber)) {
self.queuedFibers.add(fiber);
const path = self.getComponentPath(fiber.node);
//Add a functionnality to the flush function which sends a message to the window every time it is triggered.
window.top.postMessage({
source: "owl-devtools",
type: "Flush",
data: path,
origin: { frame: self.frame },
});
}
});
originalFlush.call(this, ...arguments); originalFlush.call(this, ...arguments);
inFlush = false; inFlush = false;
}; };
@@ -335,27 +376,20 @@
_render = true; _render = true;
original_Render.call(this, ...arguments); original_Render.call(this, ...arguments);
}; };
// Signals when a component is destroyed // Flush the events batcher when a root render is completed
const originalDestroy = app.root.constructor.prototype._destroy; const original_Complete = self.RootFiber.prototype.complete;
app.root.constructor.prototype._destroy = function () { self.RootFiber.prototype.complete = function () {
original_Complete.call(this, ...arguments);
if (self.recordEvents) { if (self.recordEvents) {
const path = self.getComponentPath(this); window.top.postMessage({
const event = { source: "owl-devtools",
type: "destroy", type: "Event",
component: this.name, data: self.eventsBatch,
key: this.parentKey, });
path: path, self.eventsBatch = [];
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);
} }
}; };
this.appsPatched = true;
} }
// patch reactivity system to activate subscription tracing // patch reactivity system to activate subscription tracing
@@ -419,14 +453,9 @@
} }
toggleTracing(value) { toggleTracing(value) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.traceRenderings = value; this.traceRenderings = value;
return this.traceRenderings; return this.traceRenderings;
} }
toggleSubscriptionTracing(value) { toggleSubscriptionTracing(value) {
if (value) { if (value) {
this.patchReactivity(); this.patchReactivity();
@@ -437,10 +466,6 @@
} }
// Enables/disables the recording of the render/destroy events based on value // Enables/disables the recording of the render/destroy events based on value
toggleEventsRecording(value, index) { toggleEventsRecording(value, index) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.recordEvents = value; this.recordEvents = value;
this.eventId = index; this.eventId = index;
return this.recordEvents; return this.recordEvents;
@@ -748,9 +773,6 @@
child.contentType = "object"; child.contentType = "object";
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true); child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
child.hasChildren = true; child.hasChildren = true;
if (!oldTree && type === "env") {
child.toggled = true;
}
break; break;
case "set entries": case "set entries":
case "map entries": case "map entries":
@@ -797,48 +819,57 @@
} }
break; break;
} }
if (!child.contentType) { if (child.contentType) {
if (obj === null) { if (child.toggled) {
child.content = "null"; child.children = this.loadObjectChildren(
child.contentType = "object"; child.path,
child.hasChildren = false; child.depth,
} else if (obj === undefined) { child.contentType,
child.content = "undefined"; child.objectType,
child.contentType = "undefined"; oldTree
child.hasChildren = false; );
}
return child;
}
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 instanceof Array:
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
child.contentType = "function";
child.hasChildren = true;
break;
case obj instanceof Object:
child.contentType = "object";
child.hasChildren = Object.keys(obj).length > 0;
break;
default:
child.contentType = typeof obj;
child.hasChildren = false;
}
if (key.type === "set entry") {
child.content = this.serializer.serializeItem(obj, true);
} else { } else {
obj = this.toRaw(obj); child.content = this.serializer.serializeContent(obj, child.contentType);
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 instanceof Array:
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
child.contentType = "function";
child.hasChildren = true;
break;
case obj instanceof Object:
child.contentType = "object";
child.hasChildren =
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
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) { if (child.toggled) {
@@ -850,7 +881,6 @@
oldTree oldTree
); );
} }
this.addHighlightedKeys(child);
return child; return child;
} }
@@ -860,10 +890,7 @@
let path = completePath.slice(objPathIndex); let path = completePath.slice(objPathIndex);
let obj; let obj;
if (objType === "subscription") { if (objType === "subscription") {
const subscriptionPath = completePath.slice(0, objPathIndex + 3); obj = oldTree.subscriptions.children[path[1].value].target;
obj = oldTree.subscriptions.children.find(
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
).target;
path = path.slice(3); path = path.slice(3);
} else { } else {
// Everything here is in component if it is not an app so remove this key of the path in the former case // Everything here is in component if it is not an app so remove this key of the path in the former case
@@ -893,7 +920,7 @@
const children = []; const children = [];
depth = depth + 1; depth = depth + 1;
let obj = this.getObjectProperty(path); let obj = this.getObjectProperty(path);
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType); let oldBranch = this.getObjectInOldTree(oldTree, path, objType);
if (!obj) { if (!obj) {
return []; return [];
} }
@@ -908,7 +935,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[0], oldBranch.children[0],
oldTree oldTree
); );
children.push(mapKey); children.push(mapKey);
@@ -918,7 +945,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[1], oldBranch.children[1],
oldTree oldTree
); );
children.push(mapValue); children.push(mapValue);
@@ -929,7 +956,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[0], oldBranch.children[0],
oldTree oldTree
); );
children.push(setValue); children.push(setValue);
@@ -950,7 +977,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[index], oldBranch.children[index],
oldTree oldTree
); );
if (child) { if (child) {
@@ -965,7 +992,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[index], oldBranch.children[index],
oldTree oldTree
); );
if (child) { if (child) {
@@ -982,7 +1009,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[index], oldBranch.children[index],
oldTree oldTree
); );
if (entries) { if (entries) {
@@ -996,7 +1023,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[index], oldBranch.children[index],
oldTree oldTree
); );
if (child) { if (child) {
@@ -1031,7 +1058,7 @@
depth, depth,
objType, objType,
path, path,
oldBranch?.children[index], oldBranch.children[index],
oldTree oldTree
); );
if (child) children.push(child); if (child) children.push(child);
@@ -1064,14 +1091,14 @@
}); });
proto = Object.getPrototypeOf(proto); proto = Object.getPrototypeOf(proto);
} }
if (obj.__proto__) { if (!(obj.constructor.name === "Object")) {
prototype = this.serializeObjectChild( prototype = this.serializeObjectChild(
obj, obj,
{ type: "prototype", childIndex: children.length }, { type: "prototype", childIndex: children.length },
depth, depth,
objType, objType,
path, path,
oldBranch?.children.at(-1), oldBranch.children.at(-1),
oldTree oldTree
); );
children.push(prototype); children.push(prototype);
@@ -1276,15 +1303,16 @@
children: [], children: [],
}; };
} else { } else {
const rawSubscriptions = this.topLevelSubscriptions(node); const rawSubscriptions = node.subscriptions;
component.subscriptions = { component.subscriptions = {
toggled: oldTree ? oldTree.subscriptions.toggled : true, toggled: oldTree ? oldTree.subscriptions.toggled : true,
children: [], children: [],
}; };
rawSubscriptions.forEach((rawSubscription) => { rawSubscriptions.forEach((rawSubscription, index) => {
let subscription = { let subscription = {
keys: [],
target: { target: {
name: this.targetName(rawSubscription.target, node), name: "target",
contentType: contentType:
typeof rawSubscription.target === "object" typeof rawSubscription.target === "object"
? Array.isArray(rawSubscription.target) ? Array.isArray(rawSubscription.target)
@@ -1295,20 +1323,28 @@
path: [ path: [
...path, ...path,
{ type: "item", value: "subscriptions" }, { type: "item", value: "subscriptions" },
{ type: "item", value: rawSubscription.index }, { type: "item", value: index },
{ type: "item", value: "target" }, { type: "item", value: "target" },
], ],
toggled: false, toggled: false,
objectType: "subscription", objectType: "subscription",
}, },
keysExpanded: false,
}; };
if ( if (
oldTree && oldTree &&
oldTree.subscriptions.children[rawSubscription.index] && oldTree.subscriptions.children[index] &&
oldTree.subscriptions.children[rawSubscription.index].target.toggled oldTree.subscriptions.children[index].target.toggled
) { ) {
subscription.target.toggled = true; subscription.target.toggled = true;
} }
rawSubscription.keys.forEach((key) => {
if (typeof key === "symbol") {
subscription.keys.push(key.toString());
} else {
subscription.keys.push(key);
}
});
if (rawSubscription.target == null) { if (rawSubscription.target == null) {
if (subscription.target.contentType === "undefined") { if (subscription.target.contentType === "undefined") {
subscription.target.content = "undefined"; subscription.target.content = "undefined";
@@ -1338,7 +1374,6 @@
oldTree oldTree
); );
} }
this.addHighlightedKeys(subscription.target);
component.subscriptions.children.push(subscription); component.subscriptions.children.push(subscription);
}); });
} }
@@ -1446,11 +1481,8 @@
return; return;
} }
} }
const item = path.pop(); const key = path.pop().value;
const obj = this.getObjectProperty(path); const obj = this.getObjectProperty(path);
const key = item.hasOwnProperty("symbolIndex")
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
: item.value;
if (!obj) { if (!obj) {
return; return;
} }
@@ -1524,7 +1556,7 @@
} }
// Returns the tree of components of the inspected page in a parsed format // Returns the tree of components of the inspected page in a parsed format
// Use inspectedPath to specify the path of the selected component // Use inspectedPath to specify the path of the selected component
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) { getComponentsTree(inspectedPath = null, oldTrees = null) {
const appsArray = [...this.apps]; const appsArray = [...this.apps];
const trees = appsArray.map((app, index) => { const trees = appsArray.map((app, index) => {
let oldTree; let oldTree;
@@ -1577,8 +1609,7 @@
} }
return appNode; return appNode;
}); });
const component = this.getComponentDetails(inspectedPath, oldDetails); return trees ? trees : [];
return trees ? [trees, component] : [];
} }
// Recursively fills the components tree as a parsed version // Recursively fills the components tree as a parsed version
fillTree(appNode, treeNode, inspectedPathString, oldBranch) { fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
@@ -1674,54 +1705,6 @@
inspect(obj); inspect(obj);
} }
} }
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 checkOwlStatus() { function checkOwlStatus() {