Compare commits

..

9 Commits

Author SHA1 Message Date
Géry Debongnie 4cffb23bd5 [REF] blockdom: some small optimization 2023-07-18 13:30:45 +02:00
Géry Debongnie 5154a1cc1d [REF] blockdom: micro optimizations 2023-07-17 15:01:42 +02:00
Géry Debongnie 7538aeae0e [IMP] runtime: only destroy component in raf callback
Before this commit, most of the time, components are destroyed when the
virtual dom is patched and a component node is removed. However, since
Owl is asynchronous and a component may take some time to get ready
(with onWillStart), it can happen that a component is created, then
before it is ready, it is recreated.  In that case, the initial instance
has to be destroyed.

Before this commit, the destroy operation was done immediately, when we
cancel the current fibers.  However, this means that we cannot have a
guarantee between micro task ticks that a component has not been
destroyed in the meantime.

For example, in Odoo, it is common to use the rpc service, which will
throw an error if called by a destroyed component. But because of the
possible destruction of a component at any time, the following code is
unsafe:

async loadSomeData() {
  // guaranteed to be called when component is alive
  await Promise.resolve();
  // however here, component may have been destroyed
  this.rpc(...)
}

So, to prevent this issue, we can slightly delay the destroy operation.
It is not entirely trivial, since we need to find a way to neutralize
the component in the meantime. But it seems like performing all that
kind of operation at the "commit" phase (so, the request animation frame
callback) makes sense to me.

So, this commit modifies the code to add a new component status
(cancelled) and use it to cancel components that are waiting to be
destroyed. These components will then be destroyed as soon as the
requestanimation frame starts, before all other dom operations.
2023-07-17 11:05:34 +02:00
Samuel Degueldre 3e9ba9ca8e [REF] runtime: simplify implementation of batched
Currently the implementation of batched is quite complicated and
difficult to read. This is because this approach tried to block all
calls at the same point and then only let the first one go through, but
an alternative approach is to simply throw away the calls that are made
after the first one has been scheduled. This change makes the
implementation much simpler to understand.

Co-authored-by: Aaron Bohy <aab@odoo.com>
2023-07-17 10:48:00 +02:00
Géry Debongnie e4c296a7d2 [FIX] compiler: allow t-model.number to work with select
Before this commit, owl parser would ignore the `.number` suffix on
<select> options. I do not see a good reason for that, and it prevents
some legitimate usecases.

closes #1444
2023-07-12 10:23:36 +02:00
Julien Carion (juca) 44748270da [FIX] devtools: Fix crash when no root node
This commit fixes a crash in the retrieval of the components tree which
happened when the first app did not contain a root node. The default
inspected component is now set to be the first root component found in
the apps.
2023-07-12 10:01:38 +02:00
Julien Carion (juca) 8b1dc4c43d [FIX] devtools: fix/imp env display
This commit first fixes how object prototype are detected so that it
won't stop as soon as the constructor name of the object is "Object".
This allows displaying every single prototype encountered and closes
https://github.com/odoo/owl/issues/1467.

This commit also improves how the env of a component is displayed by
expanding its chain of prototypes by default while keeping its keys lit
as long as it is their first occurence in the chain.
2023-07-05 11:27:42 +02:00
Julien Carion (juca) c105c6da38 [FIX] devtools: fix symbols handling and display
This commit fixes the three following issues:
- Symbols could never appear in shortened display of objects
- Objects which contained only symbols as keys would be considered to be
  empty and therefore not expandable
- Symbol value edition would create a new property on the object with
  the stringified symbol as key instead of updating its value

closes https://github.com/odoo/owl/issues/1464
2023-06-29 14:15:44 +02:00
Géry Debongnie 3001420a1d [REL] v2.1.4
# v2.1.3

 - [FIX] components: properly differentiate t-call subcomponents
 - [REF] devtools: Better messages forwarding
 - [FIX] devtools: Fix app methods patching
 - [DOC] Fix a code bug in the example of slots
2023-06-28 11:17:24 +02:00
35 changed files with 608 additions and 452 deletions
-2
View File
@@ -1,7 +1,5 @@
/node_modules
/dist
# compiled devtools files
/tools/devtools/assets/*.js
npm-debug.log
+1
View File
@@ -451,5 +451,6 @@ console.log(status(component));
// logs either:
// - 'new', if the component is new and has not been mounted yet
// - 'mounted', if the component is currently mounted
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
// - 'destroyed' if the component is currently destroyed
```
+50 -18
View File
@@ -175,11 +175,21 @@ function createAttrUpdater(attr) {
}
function attrsSetter(attrs) {
if (isArray(attrs)) {
setAttribute.call(this, attrs[0], attrs[1]);
if (attrs[0] === "class") {
setClass.call(this, attrs[1]);
}
else {
setAttribute.call(this, attrs[0], attrs[1]);
}
}
else {
for (let k in attrs) {
setAttribute.call(this, k, attrs[k]);
if (k === "class") {
setClass.call(this, attrs[k]);
}
else {
setAttribute.call(this, k, attrs[k]);
}
}
}
}
@@ -191,7 +201,12 @@ function attrsUpdater(attrs, oldAttrs) {
if (val === oldAttrs[1]) {
return;
}
setAttribute.call(this, name, val);
if (name === "class") {
updateClass.call(this, val, oldAttrs[1]);
}
else {
setAttribute.call(this, name, val);
}
}
else {
removeAttribute.call(this, oldAttrs[0]);
@@ -201,13 +216,23 @@ function attrsUpdater(attrs, oldAttrs) {
else {
for (let k in oldAttrs) {
if (!(k in attrs)) {
removeAttribute.call(this, k);
if (k === "class") {
updateClass.call(this, "", oldAttrs[k]);
}
else {
removeAttribute.call(this, k);
}
}
}
for (let k in attrs) {
const val = attrs[k];
if (val !== oldAttrs[k]) {
setAttribute.call(this, k, val);
if (k === "class") {
updateClass.call(this, val, oldAttrs[k]);
}
else {
setAttribute.call(this, k, val);
}
}
}
}
@@ -3875,6 +3900,10 @@ class CodeGenerator {
})
.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
*/
@@ -3952,8 +3981,7 @@ class CodeGenerator {
let { block, forceNewBlock } = ctx;
let value = ast.value;
if (value && ctx.translate !== false) {
const match = translationRE.exec(value);
value = match[1] + this.translateFn(match[2]) + match[3];
value = this.translate(value);
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
@@ -4494,11 +4522,12 @@ class CodeGenerator {
else {
let value;
if (ast.defaultValue) {
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
if (ast.value) {
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
value = `withDefault(${expr}, \`${defaultValue}\`)`;
}
else {
value = `\`${ast.defaultValue}\``;
value = `\`${defaultValue}\``;
}
}
else {
@@ -4879,10 +4908,10 @@ function parseDOMNode(node, ctx) {
let model = null;
for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr);
if (attr.startsWith("t-on")) {
if (attr === "t-on") {
throw new OwlError("Missing event name with t-on directive");
}
if (attr === "t-on" || attr === "t-on-") {
throw new OwlError("Missing event name with t-on directive");
}
if (attr.startsWith("t-on-")) {
on = on || {};
on[attr.slice(5)] = value;
}
@@ -5506,7 +5535,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.1.2";
const version = "2.1.3";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5585,6 +5614,8 @@ window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
apps: new Set(),
Fiber: Fiber,
RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
});
class App extends TemplateSet {
constructor(Root, config = {}) {
@@ -5837,8 +5868,9 @@ function useChildSubEnv(envExtension) {
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* @template T
* @param {Effect<T>} effect the effect to run on component mount and/or patch
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
* 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
* again. The default value returns an array containing only NaN because
@@ -5920,6 +5952,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 };
__info__.date = '2023-04-29T07:45:54.333Z';
__info__.hash = 'aabb755';
__info__.date = '2023-06-28T09:17:13.630Z';
__info__.hash = '432ff44';
__info__.url = 'https://github.com/odoo/owl';
-22
View File
@@ -1,22 +0,0 @@
{
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
+1 -41
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.3",
"version": "2.1.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1048,31 +1048,6 @@
"@babel/types": "^7.3.0"
}
},
"@types/chrome": {
"version": "0.0.114",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.114.tgz",
"integrity": "sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==",
"dev": true,
"requires": {
"@types/filesystem": "*",
"@types/har-format": "*"
}
},
"@types/filesystem": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz",
"integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==",
"dev": true,
"requires": {
"@types/filewriter": "*"
}
},
"@types/filewriter": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz",
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
"@types/fs-extra": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz",
@@ -1101,12 +1076,6 @@
"@types/node": "*"
}
},
"@types/har-format": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz",
"integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==",
"dev": true
},
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -3369,15 +3338,6 @@
}
}
},
"jest-chrome": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/jest-chrome/-/jest-chrome-0.8.0.tgz",
"integrity": "sha512-39RR1GT9nI4e4jsuH1vIf4l5ApxxkcstjGJr+GsOURL8f4Db0UlbRnsZaM+ZRniaGtokqklUH5VFKGZZ6YztUg==",
"dev": true,
"requires": {
"@types/chrome": "^0.0.114"
}
},
"jest-circus": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz",
+29 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.3",
"version": "2.1.4",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -21,19 +21,15 @@
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test-owl": "jest",
"test-owl:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test-owl:watch": "jest --watch",
"test-devtools": "npm run build:devtools-chrome && jest --config=tools/devtools/tests/jest.config.json",
"test-devtools:debug": "npm run build:devtools-chrome && node --inspect-brk node_modules/.bin/jest --config=tools/devtools/tests/jest.config.json --runInBand --watch --testTimeout=5000000",
"test-devtools:watch": "jest --config=tools/devtools/tests/jest.config.json --watch",
"test": "npm run test-owl && npm run test-devtools",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
"check-formatting": "npm run prettier -- --check",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
@@ -59,13 +55,13 @@
"git-rev-sync": "^3.0.2",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-chrome": "^0.8.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0",
"npm-run-all": "^4.1.5",
"prettier": "2.4.1",
"rollup": "^2.56.3",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-string": "^3.0.0",
@@ -75,6 +71,29 @@
"ts-jest": "^27.0.5",
"typescript": "4.5.2"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},
"prettier": {
"printWidth": 100,
"endOfLine": "auto"
+2 -4
View File
@@ -365,10 +365,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const typeAttr = node.getAttribute("type");
const isInput = tagName === "input";
const isSelect = tagName === "select";
const isTextarea = tagName === "textarea";
const isCheckboxInput = isInput && typeAttr === "checkbox";
const isRadioInput = isInput && typeAttr === "radio";
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
const hasLazyMod = attr.includes(".lazy");
const hasNumberMod = attr.includes(".number");
const hasTrimMod = attr.includes(".trim");
@@ -381,8 +379,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType,
hasDynamicChildren: false,
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
shouldTrim: hasTrimMod,
shouldNumberize: hasNumberMod,
};
if (isSelect) {
// don't pollute the original ctx
+59 -19
View File
@@ -93,6 +93,17 @@ function normalizeNode(node: HTMLElement | Text) {
}
}
/**
* Encode 2 numbers and 1 boolean in a number, using 31 bits:
* n1 => encoded in 16 most significant bits
* n2 => encoded in 15 next bits
* boolean => encoded in last significant bit.
* This code assumes that n1 and n2 are small enough to fit in that number of bits
*/
function encodeValue(n1: number, n2: number, b: boolean): number {
return (((n1 << 15) | n2) << 1) | (b ? 1 : 0);
}
// -----------------------------------------------------------------------------
// building a intermediate tree
// -----------------------------------------------------------------------------
@@ -306,7 +317,7 @@ interface IndexedLocation extends Location {
interface Child {
parentRefIdx: number;
afterRefIdx?: number;
afterRefIdx: number;
isOnlyChild?: boolean;
}
@@ -374,6 +385,7 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
// tree is the parentnode here
ctx.children[info.idx] = {
parentRefIdx: info.refIdx!,
afterRefIdx: 0,
isOnlyChild: true,
};
} else {
@@ -501,7 +513,6 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
}));
const locN = locations.length;
const childN = children.length;
const childrenLocs = children;
const isDynamic = refN > 0;
// these values are defined here to make them faster to lookup in the class
@@ -556,6 +567,19 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
}
if (isDynamic) {
const nextSibling = nodeGetNextSibling;
const firstChild = nodeGetFirstChild;
const bitPackedCollectors = new Uint32Array(
collectors.map((c) => {
return encodeValue(c.idx, c.prevIdx, c.getVal === nextSibling);
})
);
const childrenLocs = new Uint32Array(
children.map((c) => {
return encodeValue(c.afterRefIdx, c.parentRefIdx, Boolean(c.isOnlyChild));
})
);
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true);
// collecting references
@@ -563,12 +587,17 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
this.refs = refs;
refs[0] = el;
for (let i = 0; i < colN; i++) {
const w = collectors[i];
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
let info = bitPackedCollectors[i];
// decode info
const fn = (info & 1) === 1 ? nextSibling : firstChild;
info = info >> 1;
const prevIdx = info & 0b111111111111111;
const idx = info >> 15;
refs[idx] = fn.call(refs[prevIdx]);
}
// applying data to all update points
if (locN) {
if (locN !== 0) {
const data = this.data!;
for (let i = 0; i < locN; i++) {
const loc = locations[i];
@@ -579,15 +608,21 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
nodeInsertBefore.call(parent, el, afterNode);
// preparing all children
if (childN) {
if (childN !== 0) {
const children = this.children;
for (let i = 0; i < childN; i++) {
const child = children![i];
if (child) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child.isOnlyChild = loc.isOnlyChild;
child.mount(refs[loc.parentRefIdx] as any, afterNode);
if (child !== undefined) {
let info = childrenLocs[i];
// decode info
const isOnlyChild = info & 1;
info = info >> 1;
const parentRefIdx = info & 0b111111111111111;
const afterRefIdx = info >> 15;
const afterNode = afterRefIdx !== 0 ? refs[afterRefIdx] : null;
child.isOnlyChild = isOnlyChild as any;
child.mount(refs[parentRefIdx] as any, afterNode);
}
}
}
@@ -601,7 +636,7 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
}
const refs = this.refs!;
// update texts/attributes/
if (locN) {
if (locN !== 0) {
const data1 = this.data!;
const data2 = other.data!;
for (let i = 0; i < locN; i++) {
@@ -616,14 +651,14 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
}
// update children
if (childN) {
if (childN !== 0) {
let children1 = this.children;
const children2 = other.children;
for (let i = 0; i < childN; i++) {
const child1 = children1![i];
const child2 = children2![i];
if (child1) {
if (child2) {
if (child1 !== undefined) {
if (child2 !== undefined) {
child1.patch(child2, withBeforeRemove);
} else {
if (withBeforeRemove) {
@@ -632,10 +667,15 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
child1.remove();
children1![i] = undefined;
}
} else if (child2) {
const loc = childrenLocs[i];
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
child2.mount(refs[loc.parentRefIdx] as any, afterNode);
} else if (child2 !== undefined) {
let info = childrenLocs[i];
// decode info
info = info >> 1;
const parentRefIdx = info & 0b111111111111111;
const afterRefIdx = info >> 15;
const afterNode = afterRefIdx !== 0 ? refs[afterRefIdx] : null;
child2.mount(refs[parentRefIdx] as any, afterNode);
children1![i] = child2;
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
}
function remove(this: HTMLElement) {
delete (this as any)[eventKey];
(this as any)[eventKey] = false;
this.removeEventListener(evName, listener, { capture });
}
function update(this: HTMLElement, data: any) {
+5 -5
View File
@@ -28,7 +28,7 @@ class VList {
this.anchor = _anchor;
nodeInsertBefore.call(parent, _anchor, afterNode);
const l = children.length;
if (l) {
if (l !== 0) {
const mount = children[0].mount;
for (let i = 0; i < l; i++) {
mount.call(children[i], parent, _anchor);
@@ -186,7 +186,7 @@ class VList {
} else {
for (let i = startIdx1; i <= endIdx1; i++) {
let ch = ch1[i];
if (ch) {
if (ch !== null) {
if (withBeforeRemove) {
beforeRemove.call(ch);
}
@@ -200,7 +200,7 @@ class VList {
beforeRemove() {
const children = this.children;
const l = children.length;
if (l) {
if (l !== 0) {
const beforeRemove = children[0].beforeRemove;
for (let i = 0; i < l; i++) {
beforeRemove.call(children[i]);
@@ -215,7 +215,7 @@ class VList {
} else {
const children = this.children;
const l = children.length;
if (l) {
if (l !== 0) {
const remove = children[0].remove;
for (let i = 0; i < l; i++) {
remove.call(children[i]);
@@ -240,7 +240,7 @@ export function list(children: VNode[]): VNode<VList> {
}
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
let mapping: any = {};
const mapping: any = {};
for (let i = startIdx1; i <= endIdx2; i++) {
mapping[ch1[i].key] = i;
}
+10 -10
View File
@@ -26,7 +26,7 @@ export class VMulti {
const anchors = new Array(l);
for (let i = 0; i < l; i++) {
let child = children[i];
if (child) {
if (child !== undefined) {
child.mount(parent, afterNode);
} else {
const childAnchor = document.createTextNode("");
@@ -44,7 +44,7 @@ export class VMulti {
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
if (child) {
if (child !== undefined) {
child.moveBeforeDOMNode(node, parent);
} else {
const anchor = anchors![i];
@@ -56,14 +56,14 @@ export class VMulti {
moveBeforeVNode(other: VMulti | null, afterNode: Node | null) {
if (other) {
const next = other!.children[0];
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
afterNode = (next !== undefined ? next.firstNode() : other!.anchors![0]) || null;
}
const children = this.children;
const parent = this.parentEl;
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
if (child) {
if (child !== undefined) {
child.moveBeforeVNode(null, afterNode);
} else {
const anchor = anchors![i];
@@ -83,8 +83,8 @@ export class VMulti {
for (let i = 0, l = children1.length; i < l; i++) {
const vn1 = children1[i];
const vn2 = children2[i];
if (vn1) {
if (vn2) {
if (vn1 !== undefined) {
if (vn2 !== undefined) {
vn1.patch(vn2, withBeforeRemove);
} else {
const afterNode = vn1.firstNode()!;
@@ -97,7 +97,7 @@ export class VMulti {
vn1.remove();
children1[i] = undefined;
}
} else if (vn2) {
} else if (vn2 !== undefined) {
children1[i] = vn2;
const anchor = anchors[i];
vn2.mount(parentEl, anchor);
@@ -110,7 +110,7 @@ export class VMulti {
const children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
if (child !== undefined) {
child.beforeRemove();
}
}
@@ -125,7 +125,7 @@ export class VMulti {
const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
if (child) {
if (child !== undefined) {
child.remove();
} else {
nodeRemoveChild.call(parentEl, anchors![i]);
@@ -136,7 +136,7 @@ export class VMulti {
firstNode(): Node | undefined {
const child = this.children[0];
return child ? child.firstNode() : this.anchors![0];
return child !== undefined ? child.firstNode() : this.anchors![0];
}
toString(): string {
+18 -1
View File
@@ -145,6 +145,9 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
async render(deep: boolean) {
if (this.status >= STATUS.CANCELLED) {
return;
}
let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve();
@@ -171,7 +174,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
if (this.status >= STATUS.CANCELLED) {
return;
}
// We only want to actually render the component if the following two
@@ -190,6 +193,20 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
}
cancel() {
this._cancel();
delete this.parent!.children[this.parentKey!];
this.app.scheduler.scheduleDestroy(this);
}
_cancel() {
this.status = STATUS.CANCELLED;
const children = this.children;
for (let childKey in children) {
children[childKey]._cancel();
}
}
destroy() {
let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy();
+11 -9
View File
@@ -51,17 +51,19 @@ export function handleError(params: ErrorParams) {
);
}
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
const fiber = "fiber" in params ? params.fiber : node.fiber;
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
if (fiber) {
// resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
current = current.parent;
} while (current);
fibersInError.set(fiber.root!, error);
fibersInError.set(fiber.root!, error);
}
const handled = _handleError(node, error);
if (!handled) {
+1 -2
View File
@@ -55,8 +55,7 @@ function cancelFibers(fibers: Fiber[]): number {
let node = fiber.node;
fiber.render = throwOnRender;
if (node.status === STATUS.NEW) {
node.destroy();
delete node.parent!.children[node.parentKey!];
node.cancel();
}
node.fiber = null;
if (fiber.bdom) {
+26 -9
View File
@@ -1,3 +1,4 @@
import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status";
@@ -14,6 +15,7 @@ export class Scheduler {
requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0;
delayedRenders: Fiber[] = [];
cancelledNodes: Set<ComponentNode> = new Set();
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -23,6 +25,13 @@ export class Scheduler {
this.tasks.add(fiber.root!);
}
scheduleDestroy(node: ComponentNode) {
this.cancelledNodes.add(node);
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
/**
* Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged.
@@ -39,15 +48,23 @@ export class Scheduler {
}
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => {
this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber));
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
}
});
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
processTasks() {
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
}
this.cancelledNodes.clear();
for (let task of this.tasks) {
this.processFiber(task);
}
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
}
}
+6 -1
View File
@@ -7,15 +7,20 @@ import type { Component } from "./component";
export const enum STATUS {
NEW,
MOUNTED, // is ready, and in DOM. It has a valid el
// component has been created, but has been replaced by a newer component before being mounted
// it is cancelled until the next animation frame where it will be destroyed
CANCELLED,
DESTROYED,
}
type STATUS_DESCR = "new" | "mounted" | "destroyed";
type STATUS_DESCR = "new" | "mounted" | "cancelled" | "destroyed";
export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) {
case STATUS.NEW:
return "new";
case STATUS.CANCELLED:
return "cancelled";
case STATUS.MOUNTED:
return "mounted";
case STATUS.DESTROYED:
+1 -1
View File
@@ -30,7 +30,7 @@ function callSlot(
const slots = ctx.props.slots || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = ObjectCreate(__ctx || {});
if (__scope) {
if (__scope !== undefined) {
slotScope[__scope] = extra;
}
const slotBDom = __render ? __render(slotScope, parent, key) : null;
+7 -14
View File
@@ -9,20 +9,13 @@ export type Callback = () => void;
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
let scheduled = false;
return async (...args) => {
if (scheduled === false) {
scheduled = true;
await Promise.resolve();
scheduled = false;
callback(...args);
}
};
}
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.1.3";
export const version = "2.1.4";
+2 -2
View File
@@ -1991,8 +1991,8 @@ describe("qweb parser", () => {
baseExpr: "state",
expr: "'stuff'",
eventType: "click",
shouldNumberize: false,
shouldTrim: false,
shouldNumberize: true,
shouldTrim: true,
targetAttr: "value",
hasDynamicChildren: false,
specialInitTargetAttr: "checked",
@@ -212,6 +212,73 @@ exports[`changing state before first render does not trigger a render 1`] = `
}"
`;
exports[`component destroyed just after render 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`component destroyed just after render 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = text(ctx['state'].value);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].flag) {
b3 = comp1({}, key + \`__1\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`C\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`C\`);
}
}"
`;
exports[`concurrent renderings scenario 1 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -468,6 +468,36 @@ exports[`t-model directive t-model on select with static options 1`] = `
}"
`;
exports[`t-model directive t-model with dynamic number values on select options in foreach 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { toNumber, prepareList, withKey } = helpers;
let block1 = createBlock(\`<select block-handler-0=\\"change\\"><block-child-0/></select>\`);
let block3 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'value';
const bValue1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = toNumber(ev.target.value); }];
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['state'].options);;
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`o\`] = v_block2[i1];
const key1 = ctx['o'].value;
let attr1 = ctx['o'].value;
let attr2 = bValue1 === ctx['o'].value;
let txt1 = ctx['o'].value;
c_block2[i1] = withKey(block3([attr1, attr2, txt1]), key1);
}
const b2 = list(c_block2);
return block1([hdlr1], [b2]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = `
"function anonymous(app, bdom, helpers
) {
+123 -18
View File
@@ -115,13 +115,7 @@ test("destroying/recreating a subwidget with different props (if start is not ov
await nextMicroTick();
expect(n).toBe(2);
expect([
"Child:willDestroy",
"W:willRender",
"Child:setup",
"Child:willStart",
"W:rendered",
]).toBeLogged();
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
def.resolve();
await nextTick();
@@ -130,6 +124,7 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect([
"Child:willRender",
"Child:rendered",
"Child:willDestroy",
"W:willPatch",
"Child:mounted",
"W:patched",
@@ -178,13 +173,13 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willDestroy",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
@@ -251,13 +246,13 @@ test("creating two async components, scenario 1", async () => {
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"ChildA:willDestroy",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildB:setup",
"ChildB:willStart",
"Parent:rendered",
"ChildA:willDestroy",
]).toBeLogged();
defB.resolve();
@@ -703,13 +698,13 @@ test("rendering component again in next microtick", async () => {
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willDestroy",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
@@ -1732,9 +1727,9 @@ test("concurrent renderings scenario 10", async () => {
expect(fixture.innerHTML).toBe("<div><p></p></div>");
expect([
"ComponentA:willRender",
"ComponentC:willDestroy",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
"ComponentC:willDestroy",
]).toBeLogged();
defB.resolve();
@@ -2282,7 +2277,6 @@ test("concurrent renderings scenario 16", async () => {
"D:setup",
"D:willStart",
"C:rendered",
"D:willDestroy",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
@@ -2290,6 +2284,7 @@ test("concurrent renderings scenario 16", async () => {
"D:setup",
"D:willStart",
"C:rendered",
"D:willDestroy",
]).toBeLogged();
// at this point, C rendering is still pending, and nothing should have been
@@ -2997,11 +2992,11 @@ test("t-key on dom node having a component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (3):mounted",
@@ -3055,11 +3050,11 @@ test("t-key on dynamic async component (toggler is never patched)", async () =>
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (3):mounted",
@@ -3114,11 +3109,11 @@ test("t-foreach with dynamic async component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (3):mounted",
@@ -3801,7 +3796,7 @@ test("destroyed component causes other soon to be destroyed component to rerende
static template = xml`<t t-esc="state.val + props.value"/>`;
state = useState({ val: 0 });
setup() {
c = this;
c = c || this;
useLogLifecycle();
}
}
@@ -3846,8 +3841,6 @@ test("destroyed component causes other soon to be destroyed component to rerende
parent.state.valueB = 2;
await nextTick();
expect([
"B:willDestroy",
"C:willDestroy",
"A:willRender",
"B:setup",
"B:willStart",
@@ -3858,6 +3851,8 @@ test("destroyed component causes other soon to be destroyed component to rerende
"B:rendered",
"C:willRender",
"C:rendered",
"B:willDestroy",
"C:willDestroy",
"A:willPatch",
"C:mounted",
"B:mounted",
@@ -4200,6 +4195,116 @@ test("delayed render is not cancelled by upcoming render", async () => {
]).toBeLogged();
});
test("components are not destroyed between animation frame", async () => {
const def = makeDeferred();
class C extends Component {
static template = xml`C`;
setup() {
useLogLifecycle();
}
}
class B extends Component {
static template = xml`B<C/>`;
static components = { C };
setup() {
useLogLifecycle();
onWillStart(() => {
return def;
});
}
}
class A extends Component {
static template = xml`A<B t-if="state.flag"/>`;
static components = { B };
state = useState({ flag: false });
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A");
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
// turn the flag on, this will render A and stops at B because of def
a.state.flag = true;
await nextTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
// force a render of A
// => owl will need to create a new B component
// => initial B component will be cancelled
a.render();
await nextMicroTick();
expect([
// note that B is not destroyed here. It is cancelled instead
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
]).toBeLogged();
// resolve def, so B render is unblocked
def.resolve();
await nextTick();
expect([
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
// animation frame callback starts here
"B:willDestroy", // B is destroyed here
"A:willPatch",
"C:mounted",
"B:mounted",
"A:patched",
]).toBeLogged();
});
test("component destroyed just after render", async () => {
let stateB: any;
class B extends Component {
static template = xml`B<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
stateB = this.state;
useLogLifecycle();
}
}
class A extends Component {
static template = xml`<B/>`;
static components = { B };
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("B1");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"B:mounted",
"A:mounted",
]).toBeLogged();
stateB!.value++; // force a render of B
await nextMicroTick(); // wait for B render to actually start
a.__owl__.app.destroy();
expect(["A:willUnmount", "B:willUnmount", "B:willDestroy", "A:willDestroy"]).toBeLogged();
await nextTick();
// check that B was not rendered after being destroyed
expect([]).toBeLogged();
});
// test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 };
+6 -2
View File
@@ -1444,6 +1444,7 @@ describe("can catch errors", () => {
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
@@ -1506,12 +1507,15 @@ describe("can catch errors", () => {
parent.state.hasChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1");
await nextTick();
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
});
+33
View File
@@ -626,6 +626,39 @@ describe("t-model directive", () => {
expect(fixture.querySelector("select")!.value).toEqual("b");
});
test("t-model with dynamic number values on select options in foreach", async () => {
class Test extends Component {
static template = xml`
<select t-model.number="state.value">
<t t-foreach="state.options" t-as="o" t-key="o.value">
<option t-att-value="o.value" t-esc="o.value"/>
</t>
</select>
`;
state: any;
setup() {
this.state = useState({
value: 2,
options: [{ value: 1 }, { value: 2 }, { value: 3 }],
});
}
}
const comp = await mount(Test, fixture);
// check that we have a value of 2 selected
expect(fixture.querySelector("select")!.value).toEqual("2");
expect(comp.state.value).toBe(2);
// emulate a click on the option=3 element
fixture.querySelectorAll("option")[2].selected = true;
fixture.querySelector("select")!.dispatchEvent(new Event("change"));
await nextTick();
// check that we have now selected the number 3 (and not the string)
expect(fixture.querySelector("select")!.value).toEqual("3");
expect(comp.state.value).toBe(3);
});
test("t-model is applied before t-on-input", async () => {
expect.assertions(3);
class SomeComponent extends Component {
+13 -9
View File
@@ -1,6 +1,7 @@
import terser from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import execute from "rollup-plugin-execute";
import del from "rollup-plugin-delete";
import { string } from "rollup-plugin-string";
const isWindows = process.platform === "win32";
@@ -36,12 +37,12 @@ export default ({ "config-browser": browser, "config-env": env }) => {
},
];
function generateRule(input, plugins = []) {
function generateRule(input, format = "esm") {
return {
input,
input: input,
output: [
{
format: "esm",
format: format,
file: input.replace("tools/devtools/src", "dist/devtools"),
},
],
@@ -50,7 +51,6 @@ export default ({ "config-browser": browser, "config-env": env }) => {
include: "**/page_scripts/owl_devtools_global_hook.js",
}),
isProduction && terser.terser(),
...plugins,
],
};
}
@@ -58,6 +58,7 @@ export default ({ "config-browser": browser, "config-env": env }) => {
commands[1] = isWindows
? "npm run compile_templates -- tools\\devtools\\src && move templates.js tools\\devtools\\assets\\templates.js"
: "npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
const firstRule = generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js");
if (isProduction) {
commands[0] = isWindows
? "npm run build && copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js && npm run build:compiler"
@@ -67,16 +68,19 @@ export default ({ "config-browser": browser, "config-env": env }) => {
? "copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js"
: "cp dist/owl.iife.js tools/devtools/assets/owl.js";
}
firstRule.plugins.push(execute(commands, true));
const secondRule = generateRule("tools/devtools/src/content.js");
secondRule.plugins.push(copy({ targets: filesToMove }));
const lastRule = generateRule("tools/devtools/src/background.js");
lastRule.plugins.push(del({ targets: "tools/devtools/assets/*.js" }));
return [
generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js", [
execute(commands, true),
]),
generateRule("tools/devtools/src/content.js", [copy({ targets: filesToMove })]),
firstRule,
secondRule,
generateRule("tools/devtools/src/devtools_app/devtools.js"),
generateRule("tools/devtools/src/utils.js"),
generateRule("tools/devtools/src/devtools_app/devtools_panel.js"),
generateRule("tools/devtools/src/popup_app/popup.js"),
generateRule("tools/devtools/src/background.js"),
lastRule,
];
};
@@ -3,10 +3,8 @@
import { DevtoolsWindow } from "./devtools_window/devtools_window";
const { mount } = owl;
import { templates } from "../../assets/templates.js";
import { createStore } from "./store/store";
for (const template in templates) {
owl.App.registerTemplate(template, templates[template]);
}
createStore();
mount(DevtoolsWindow, document.body, { dev: true });
@@ -47,7 +47,7 @@ export class ObjectTreeElement extends Component {
classFor(object) {
// Prototype items will be dyed down to appear less important
if (object.path.some((item) => item?.type === "prototype")) {
if (object.path.some((item) => item?.type === "prototype") && !object.keepLit) {
return "attenuate";
}
// Same for subscription items which are not present in the keys while the keys will be bold
+35 -5
View File
@@ -102,7 +102,7 @@ export const store = reactive({
if (IS_FIREFOX) {
await evalInWindow("window.$0 = $0;", this.activeFrame);
}
const [apps, component] = await evalFunctionInWindow(
const [apps, details] = await evalFunctionInWindow(
"getComponentsTree",
fromOld && this.activeComponent
? [this.activeComponent.path, this.apps, this.activeComponent]
@@ -113,7 +113,8 @@ export const store = reactive({
if (!fromOld && this.settings.expandByDefault) {
this.apps.forEach((tree) => expandNodes(tree, true));
}
this.activeComponent = component;
keepEnvLit(details);
this.activeComponent = details;
},
// Select a component by retrieving its details from the page based on its path
@@ -150,9 +151,11 @@ export const store = reactive({
[component.path],
this.activeFrame
);
this.activeComponent = details;
if (!this.activeComponent) {
if (!details) {
await this.loadComponentsTree(false);
} else {
keepEnvLit(details);
this.activeComponent = details;
}
if (this.page !== "ComponentsTab") {
this.switchTab("ComponentsTab");
@@ -625,7 +628,9 @@ export function useStore() {
return useState(store);
}
export async function createStore() {
init();
async function init() {
store.devtoolsId = await getTabURL();
evalFunctionInWindow("initDevtools", []);
@@ -885,6 +890,31 @@ 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
function foldNodes(node) {
node.toggled = false;
@@ -33,14 +33,11 @@
* 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,
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
});
}
}
});
@@ -120,6 +117,15 @@
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);
}
return "{" + result.join(", ") + "}";
},
map(obj) {
@@ -177,24 +183,18 @@
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 },
},
"/"
);
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,
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
});
self.eventsBatch = [];
}
};
@@ -207,23 +207,17 @@
const originalDelete = this.apps.delete;
this.apps.add = function () {
originalAdd.call(this, ...arguments);
window.top.postMessage(
{
source: "owl-devtools",
type: "RefreshApps",
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
};
this.apps.delete = function () {
originalDelete.call(this, ...arguments);
window.top.postMessage(
{
source: "owl-devtools",
type: "RefreshApps",
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
};
}
@@ -617,14 +611,11 @@
const path = this.getElementPath(target);
this.highlightComponent(path);
this.currentSelectedElement = target;
window.top.postMessage(
{
source: "owl-devtools",
type: "SelectElement",
data: path,
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "SelectElement",
data: path,
});
}
};
@@ -648,13 +639,10 @@
document.removeEventListener("mouseover", this.HTMLSelector, { capture: true });
document.removeEventListener("click", this.disableHTMLSelector, { capture: true });
document.removeEventListener("mouseout", this.removeHighlights, { capture: true });
window.top.postMessage(
{
source: "owl-devtools",
type: "StopSelector",
},
"/"
);
window.top.postMessage({
source: "owl-devtools",
type: "StopSelector",
});
};
// Returns the object specified by the path starting from the topParent object
@@ -760,6 +748,9 @@
child.contentType = "object";
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
child.hasChildren = true;
if (!oldTree && type === "env") {
child.toggled = true;
}
break;
case "set entries":
case "map entries":
@@ -836,7 +827,8 @@
break;
case obj instanceof Object:
child.contentType = "object";
child.hasChildren = Object.keys(obj).length > 0;
child.hasChildren =
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
break;
default:
child.contentType = typeof obj;
@@ -901,7 +893,7 @@
const children = [];
depth = depth + 1;
let obj = this.getObjectProperty(path);
let oldBranch = this.getObjectInOldTree(oldTree, path, objType);
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
if (!obj) {
return [];
}
@@ -916,7 +908,7 @@
depth,
objType,
path,
oldBranch.children[0],
oldBranch?.children[0],
oldTree
);
children.push(mapKey);
@@ -926,7 +918,7 @@
depth,
objType,
path,
oldBranch.children[1],
oldBranch?.children[1],
oldTree
);
children.push(mapValue);
@@ -937,7 +929,7 @@
depth,
objType,
path,
oldBranch.children[0],
oldBranch?.children[0],
oldTree
);
children.push(setValue);
@@ -958,7 +950,7 @@
depth,
objType,
path,
oldBranch.children[index],
oldBranch?.children[index],
oldTree
);
if (child) {
@@ -973,7 +965,7 @@
depth,
objType,
path,
oldBranch.children[index],
oldBranch?.children[index],
oldTree
);
if (child) {
@@ -990,7 +982,7 @@
depth,
objType,
path,
oldBranch.children[index],
oldBranch?.children[index],
oldTree
);
if (entries) {
@@ -1004,7 +996,7 @@
depth,
objType,
path,
oldBranch.children[index],
oldBranch?.children[index],
oldTree
);
if (child) {
@@ -1039,7 +1031,7 @@
depth,
objType,
path,
oldBranch.children[index],
oldBranch?.children[index],
oldTree
);
if (child) children.push(child);
@@ -1072,14 +1064,14 @@
});
proto = Object.getPrototypeOf(proto);
}
if (!(obj.constructor.name === "Object")) {
if (obj.__proto__) {
prototype = this.serializeObjectChild(
obj,
{ type: "prototype", childIndex: children.length },
depth,
objType,
path,
oldBranch.children.at(-1),
oldBranch?.children.at(-1),
oldTree
);
children.push(prototype);
@@ -1454,8 +1446,11 @@
return;
}
}
const key = path.pop().value;
const item = path.pop();
const obj = this.getObjectProperty(path);
const key = item.hasOwnProperty("symbolIndex")
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
: item.value;
if (!obj) {
return;
}
@@ -1524,8 +1519,9 @@
}
}
}
// If nothing was found, return the first app's root component path
return ["0", "root"];
// 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
@@ -1739,7 +1735,7 @@
owlStatus = -1;
}
}
window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus }, "/");
window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus });
}
if (!window.__OWL_DEVTOOLS__) {
@@ -1761,19 +1757,19 @@
if (value?.Fiber !== undefined) {
window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
}
window.top.postMessage({ source: "owl-devtools", type: "FrameReady" }, "/");
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" }, "/");
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" }, "/");
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(
-9
View File
@@ -1,9 +0,0 @@
import type * as owl from "../../../src/runtime";
import type { JestChrome } from "jest-chrome/types/jest-chrome";
declare global {
interface Window {
owl: typeof owl;
browser: any;
chrome: JestChrome;
}
}
-15
View File
@@ -1,15 +0,0 @@
import { mount } from "../../../src";
import { makeTestFixture } from "../../../tests/helpers";
import { DevtoolsWindow } from "../src/devtools_app/devtools_window/devtools_window";
let fixture = makeTestFixture();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("devtools", () => {
test("mounting the devtools", async () => {
await mount(DevtoolsWindow as any, fixture);
expect(fixture.querySelector(".status-message")!.textContent).toContain("There are no apps currently running.")
});
});
-28
View File
@@ -1,28 +0,0 @@
{
"testEnvironment": "jsdom",
"roots": [
"<rootDir>"
],
"setupFiles": [
"<rootDir>/../../../tests/mocks/mockEventTarget.js",
"<rootDir>/setup.ts"
],
"globals": {
"ts-jest": {
"tsconfig": "<rootDir>/tsconfig.json"
}
},
"transform": {
"^.+\\.[jt]s?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
-9
View File
@@ -1,9 +0,0 @@
import * as owl from "../../../src/runtime";
window.owl = owl;
import { chrome } from "jest-chrome";
window.chrome = chrome;
import { templates } from "../assets/templates";
for (const template in templates) {
owl.App.registerTemplate(template, templates[template as keyof typeof templates]);
}
-109
View File
@@ -1,109 +0,0 @@
{
/**
** Commented-out options have their default values.
**/
"include": [
"**/*.ts",
],
                                                              // "exclude": [],
// "files": [],                   // A list of relative or absolute file paths to include.
// "extends": "",                   // A string containing a path to another configuration file to inherit from.
// "references": [],                   // An array of objects `{"path": "./to/dirOrConfig"}` that specifies projects to reference.
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": {
                                                            // Main options
"target": "es2019",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "es6",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
"allowJs": true,                  // Allow javascript files to be compiled.
// "checkJs": false,                 // Report errors in .js files.
// "outFile": "./",                 // Concatenate and emit output to single file.
"outDir": "dist",                                           // Redirect output structure to the directory.
// "rootDir": "./",                 // Specify the root directory of input files. Use to control the output directory structure with `--outDir`.
// "project": "",                 // Compile a project given a valid configuration file.
                                                            // Compilation options
// "composite": true,                 // Enable project compilation
// "diagnostics": false,                 // Show diagnostic information.
// "incremental": true,                 // Enable incremental compilation by reading/writing information from prior compilations to a file on disk.
// "isolatedModules": false,                 // Transpile each file as a separate module (similar to 'ts.transpileModule').
// "listEmittedFiles": false,                 // Print names of generated files part of the compilation.
// "listFiles": true,                 // Print names of files part of the compilation.
// "noErrorTruncation": false,                 // Do not truncate error messages.
// "preserveWatchOutput": false,                 // Keep outdated console output in watch mode instead of clearing the screen.
// "traceResolution": false,                 // Enable tracing of the name resolution process.
// "tsBuildInfoFile": ".tsbuildinfo",                 // Specify file to store incremental compilation information.
                                                            // Strict typechecking options
"strict": true,                                          // Enable all strict type-checking options.
// "noImplicitAny": true,                 // Raise error on expressions and declarations with an implied 'any' type.
// "noImplicitThis": true,                 // Raise error on 'this' expressions with an implied 'any' type.
// "strictBindCallApply": true,                 // Enable stricter checking of of the `bind`, `call`, and `apply` methods on functions.
// "strictFunctionTypes": true,                 // Disable bivariant parameter checking for function types.
// "strictNullChecks": true,                 // In strict null checking mode, the null and undefined values are not in the domain of every type and are only assignable to themselves and any.
// "strictPropertyInitialization": true,                 // Ensure non-undefined class properties are initialized in the constructor. This option requires `--strictNullChecks` be enabled in order to take effect.
// "alwaysStrict": true,                 // Parse in strict mode and emit "use strict" for each source file.
                                                            // Additional checks
// "allowUnreachableCode": false,                 // Do not report errors on unreachable code.
// "allowUnusedLabels": false,                 // Do not report errors on unused labels.
"forceConsistentCasingInFileNames": true,                   // Disallow inconsistently-cased references to the same file.
// "noStrictGenericChecks": false,                 // Disable strict checking of generic signatures in function types.
"noUnusedLocals": true,                                     // Report errors on unused locals.
"noUnusedParameters": false,                                // Report errors on unused parameters.
"noImplicitReturns": true,                                  // Report error when not all code paths in function return a value.
"noFallthroughCasesInSwitch": true,                         // Report errors for fallthrough cases in switch statement.
// "skipLibCheck": false,                 // Skip type checking of all declaration files (*.d.ts).
// "suppressExcessPropertyErrors": false,                 // Suppress excess property checks for object literals.
// "suppressImplicitAnyIndexErrors": false,                 // Suppress noImplicitAny errors for indexing objects lacking index signatures.
                                                            // Module resolution options
"moduleResolution": "node",                                 // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
// "baseUrl": "./",                 // Base directory to resolve non-absolute module names.
// "paths": {},                 // A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
// "rootDirs": [],                 // List of root folders whose combined content represents the structure of the project at runtime.
// "typeRoots": [
// "./node_modules/@types",
// "./tools/devtools/tests"
// ],                 // List of folders to include type definitions from.
"types": [
"jest",
"node"
],                                                // Type declaration files to be included in compilation.
// "allowSyntheticDefaultImports": false                    // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
"esModuleInterop": true,                  // Emit '__importStar' and '__importDefault' helpers for runtime babel ecosystem compatibility and enable '--allowSyntheticDefaultImports' for typesystem compatibility.
// "maxNodeModuleJsDepth": 0,                 // The maximum dependency depth to search under node_modules and load JavaScript files. Only applicable with --allowJs.
// "preserveSymlinks": false,                 // Do not resolve the real path of symlinks.
"resolveJsonModule": true,                                  // Include modules imported with '.json' extension.
                                                            // Emit options
"declaration": true,                                        // Generates corresponding '.d.ts' file.
"declarationDir": "dist/types",                             // Output directory for generated declaration files.
// "declarationMap": false,                 // Generates a sourcemap for each corresponding '.d.ts' file.
// "emitBOM": false,                 // Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
// "emitDeclarationOnly": false,                 // Only emit .d.ts declaration files.
// "importHelpers": false,                 // Import emit helpers from 'tslib'.
// "newLine": "LF",                 // Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).
// "noEmit": true,                 // Do not emit outputs.
// "noEmitHelpers": false,                 // Do not generate custom helper functions like __extends in compiled output.
// "noEmitOnError": false,                 // Do not emit outputs if any errors were reported.
// "noImplicitUseStrict": false,                 // Do not emit "use strict" directives in module output.
// "noResolve": false,                 // Do not add triple-slash references or module import targets to the list of compiled files.
"preserveConstEnums": false,                                 // Do not erase const enum declarations in generated code.
// "removeComments": false,                 // Remove all comments except copy-right header comments beginning with
// "experimentalDecorators": true,                 // Enables experimental support for ES7 decorators.
// "emitDecoratorMetadata": true,                 // Enables experimental support for emitting type metadata for decorators.
                                                            // Source map options
// "sourceMap": false,                 // Generates corresponding '.map' file.
// "sourceRoot": "",                 // Specify the location where debugger should locate TypeScript files instead of source locations.
// "mapRoot": "",                 // Specify the location where debugger should locate map files instead of generated locations.
// "inlineSourceMap": true,                 // Emit a single file with source maps instead of having a separate file.
// "inlineSources": true,                 // Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set.
                                                            // JSX options
// "jsx": "preserve",                 // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
// "jsxFactory": "React.createElement",                 // Specify the JSX factory function to use when targeting react JSX emit, e.g. 'React.createElement' or 'h'.
                                                            // Other options
// "allowUmdGlobalAccess": true,                 // Allow accessing UMD globals from modules.
// "charset": "utf8",                 // The character set of the input files.
// "downlevelIteration": false,                 // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
// "disableSizeLimit": false,                 // Disable size limitation on JavaScript project.
// "keyofStringsOnly": false,                 // Resolve 'keyof' to string valued property names only (no numbers or symbols).
// "noLib": false,                 // Do not include the default library file (lib.d.ts).
// "pretty": true,                 // Stylize errors and messages using color and context.
}
}