Compare commits

..

5 Commits

Author SHA1 Message Date
Romeo Fragomeli 26c7856d5d [REL] v2.4.1
# v2.4.1

 - [FIX] app: make subroots more robust
 - [IMP] devtools: show objects class name and add special extension cases
 - [IMP] devtools: allow expansion of empty class
 - [IMP] Bump support for Node 20+
2024-10-31 10:42:38 +01:00
Géry Debongnie b8d09e523d [FIX] app: make subroots more robust
This commit fixes two issues with subroots:

1. creating a subroot create a new component node synchronously. This
   would causes issues if the creation was done in the setup of a
component, since in that case, owl would reset the current component to
null right after, which would cause all calls to hooks to fail. This is
fixed by restoring the previous component node right after creating a
root.

2. the destroy method for roots calls the scheduler processTasks.
   However, the processTasks method was not safe to reentrant calls,
which would in some cases crashes owl. For example, if a destroy is done
while a new component is mounted, the mount method would be called
twice.

This is fixed by ignoring the processTasks if we are currently
processing tasks. It works because the "for ... of" loop will still
process all new tasks in the current iteration.
2024-10-21 14:28:27 +02:00
Julien Carion (juca) 04c2808701 [IMP] devtools: show objects class name and add special extension cases
This commit ensures that class names are displayed for non generic
objects and extensions of generic classes will have custom display for
their content.
2024-10-11 10:39:50 +02:00
Julien Carion (juca) 15c2604df1 [IMP] devtools: allow expansion of empty class
This commit makes it possible to expand empty classes to see their
prototype instead of displaying them as simple empty objects.
2024-10-11 10:39:50 +02:00
Romeo Fragomeli 3e11fe6b12 [IMP] Bump support for Node 20+
This commit bump the support for Node 20+.
2024-10-09 20:31:31 +02:00
16 changed files with 260 additions and 34 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v2
+17 -3
View File
@@ -2270,6 +2270,12 @@ function collectionsProxyHandler(target, callback, targetRawType) {
}
let currentNode = null;
function saveCurrent() {
let n = currentNode;
return () => {
currentNode = n;
};
}
function getCurrent() {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
@@ -5557,7 +5563,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.4.0";
const version = "2.4.1";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5568,6 +5574,7 @@ class Scheduler {
this.frame = 0;
this.delayedRenders = [];
this.cancelledNodes = new Set();
this.processing = false;
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
addFiber(fiber) {
@@ -5598,6 +5605,10 @@ class Scheduler {
}
}
processTasks() {
if (this.processing) {
return;
}
this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -5611,6 +5622,7 @@ class Scheduler {
this.tasks.delete(task);
}
}
this.processing = false;
}
processFiber(fiber) {
if (fiber.root !== fiber) {
@@ -5686,7 +5698,9 @@ class App extends TemplateSet {
if (config.env) {
this.env = config.env;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
@@ -6018,6 +6032,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, batched, 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 = '2024-09-30T08:49:29.420Z';
__info__.hash = 'eb2b32a';
__info__.date = '2024-10-31T09:42:30.824Z';
__info__.hash = 'b8d09e5';
__info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.4.0",
"version": "2.4.1",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -9,7 +9,7 @@
"dist"
],
"engines": {
"node": ">=12.18.3"
"node": ">=20.0.0"
},
"scripts": {
"build:bundle": "rollup -c --failAfterWarnings",
+5 -2
View File
@@ -1,6 +1,6 @@
import { version } from "../version";
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { ComponentNode, saveCurrent } from "./component_node";
import { nodeErrorHandlers, handleError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, RootFiber, MountOptions } from "./fibers";
@@ -52,7 +52,7 @@ declare global {
}
}
interface Root<P, E> {
interface Root<P extends Props, E> {
node: ComponentNode<P, E>;
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
destroy(): void;
@@ -120,7 +120,10 @@ export class App<
if (config.env) {
this.env = config.env as any;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
+7
View File
@@ -10,6 +10,13 @@ import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null;
export function saveCurrent() {
let n = currentNode;
return () => {
currentNode = n;
};
}
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
+6
View File
@@ -16,6 +16,7 @@ export class Scheduler {
frame: number = 0;
delayedRenders: Fiber[] = [];
cancelledNodes: Set<ComponentNode> = new Set();
processing = false;
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -53,6 +54,10 @@ export class Scheduler {
}
processTasks() {
if (this.processing) {
return;
}
this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -66,6 +71,7 @@ export class Scheduler {
this.tasks.delete(task);
}
}
this.processing = false;
}
processFiber(fiber: RootFiber) {
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.4.0";
export const version = "2.4.1";
+27
View File
@@ -43,6 +43,33 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
exports[`app can call processTask twice in a row without crashing 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`app can call processTask twice in a row without crashing 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`app can configure an app with props 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1,5 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`destroy a subroot while another component is mounted in main app 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`ChildB\`, true, false, false, []);
const comp2 = app.createComponent(\`ChildA\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
if (ctx['state'].flag) {
b2 = comp1({}, key + \`__1\`, node, this, null);
} else {
b3 = comp2({}, key + \`__2\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`destroy a subroot while another component is mounted in main app 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<div block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`a\`);
let ref1 = (el) => this.__owl__.setRef((\`elem\`), el);
const b3 = block3([ref1]);
return multi([b2, b3]);
}
}"
`;
exports[`destroy a subroot while another component is mounted in main app 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[`destroy a subroot while another component is mounted in main app 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`b\`);
}
}"
`;
exports[`subroot by default, env is the same in sub root 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -26,6 +83,28 @@ exports[`subroot by default, env is the same in sub root 2`] = `
}"
`;
exports[`subroot can create a root in a setup function, then use a hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`a\`);
}
}"
`;
exports[`subroot can create a root in a setup function, then use a hook 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`c\`);
}
}"
`;
exports[`subroot can mount subroot 1`] = `
"function anonymous(app, bdom, helpers
) {
+18 -1
View File
@@ -1,4 +1,4 @@
import { App, Component, mount, onWillStart, useState, xml } from "../../src";
import { App, Component, mount, onWillPatch, onWillStart, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
import {
makeTestFixture,
@@ -184,4 +184,21 @@ describe("app", () => {
expect(Object.keys(app.templates)).toEqual(["hello"]);
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
});
test("can call processTask twice in a row without crashing", async () => {
class Child extends Component {
static template = xml`<div/>`;
setup() {
onWillPatch(() => app.scheduler.processTasks());
}
}
class SomeComponent extends Component {
static template = xml`parent<Child/>`;
static components = { Child };
}
const app = new App(SomeComponent);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("parent<div></div>");
});
});
+63 -2
View File
@@ -1,6 +1,6 @@
import { App, Component, xml } from "../../src";
import { App, Component, onMounted, onWillDestroy, useRef, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
import { makeTestFixture, snapshotEverything } from "../helpers";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
@@ -112,4 +112,65 @@ describe("subroot", () => {
expect(status(comp)).not.toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
test("can create a root in a setup function, then use a hook", async () => {
class C extends Component {
static template = xml`c`;
}
class A extends Component {
static template = xml`a`;
state: any;
setup() {
app.createRoot(C);
this.state = useState({ value: 1 });
}
}
const app = new App(A);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("a");
});
});
test("destroy a subroot while another component is mounted in main app", async () => {
class C extends Component {
static template = xml`c`;
}
class ChildA extends Component {
static template = xml`a<div t-ref="elem"></div>`;
ref: any;
setup() {
this.ref = useRef("elem");
let root = app.createRoot(C);
onMounted(() => {
root.mount(this.ref.el);
});
onWillDestroy(() => {
root.destroy();
});
}
}
class ChildB extends Component {
static template = xml`b`;
}
class SomeComponent extends Component {
static template = xml`
<t t-if="state.flag"><ChildB/></t>
<t t-else=""><ChildA/></t>
`;
static components = { ChildA, ChildB };
state = useState({ flag: false });
}
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("a<div></div>");
await nextTick();
expect(fixture.innerHTML).toBe("a<div>c</div>");
comp.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("b");
});
+1 -1
View File
@@ -48,7 +48,7 @@ describe("basic validation", () => {
test("compilation error", () => {
const template = `<div t-att-class="a b">test</div>`;
expect(() => renderToString(template))
.toThrow(`Failed to compile anonymous template: Unexpected identifier
.toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
+2 -2
View File
@@ -157,7 +157,7 @@ describe("basics", () => {
} catch (e) {
error = e as Error;
}
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
@@ -182,7 +182,7 @@ function(app, bdom, helpers) {
static components = { Child };
static template = xml`<Child/>`;
}
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
@@ -109,23 +109,34 @@
object(obj) {
const result = [];
let length = 0;
for (const [key, value] of Object.entries(obj)) {
if (length > 25) {
result.push("...");
break;
if (obj instanceof String) {
result[0] = `'${obj.toString()}'`;
} else if (obj instanceof Array) {
return `${obj.constructor.name} ${this.array([...obj])}`;
} else if (obj instanceof Number) {
result[0] = obj.toString();
} else {
for (const [key, value] of Object.entries(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key + ": " + this.serializeItem(value);
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);
}
const element = key + ": " + this.serializeItem(value);
length += element.length;
result.push(element);
}
for (const key of Object.getOwnPropertySymbols(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key.toString() + ": " + this.serializeItem(obj[key]);
length += element.length;
result.push(element);
if (obj.constructor.name !== "Object") {
return obj.constructor.name + " {" + result.join(", ") + "}";
}
return "{" + result.join(", ") + "}";
},
@@ -823,7 +834,7 @@
child.contentType = "set";
child.hasChildren = true;
break;
case obj instanceof Array:
case obj.constructor.name === "Array":
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
@@ -834,7 +845,9 @@
case obj instanceof Object:
child.contentType = "object";
child.hasChildren =
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
Object.keys(obj).length ||
Object.getOwnPropertySymbols(obj).length ||
obj.constructor.name !== "Object";
break;
default:
child.contentType = typeof obj;
-1
View File
@@ -140,7 +140,6 @@ async function startRelease() {
}
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js dist/*.zip ${draft} -F ${file}`);
if (relaseResult !== 0) {