فهرست منبع

fixes reactivity

Matheus Giovani 2 سال پیش
والد
کامیت
43d0b5515e

+ 1 - 2
.npmignore

@@ -3,8 +3,7 @@ test/
 modules/
 tsconfig.json
 webpack.config.js
-yarn-error.log
-yarn.lock
+*-error.log
 node_modules
 modules/
 .local

+ 3 - 6
package.json

@@ -17,15 +17,12 @@
     "watch:renderer": "cd packages/renderer && yarn watch",
     "watch:test": "nodemon"
   },
-  "dependencies": {
-    "alpinejs": "^3.10.2",
-    "pug": "^3.0.2"
-  },
+  "dependencies": {},
   "devDependencies": {
-    "npm-run-all": "^4.1.5",
     "source-map-loader": "^3.0.0",
     "source-map-support": "^0.5.21",
     "webpack": "^5.51.1",
-    "webpack-cli": "^4.8.0"
+    "webpack-cli": "^4.8.0",
+    "yarn-run-all": "^3.1.1"
   }
 }

+ 0 - 3
packages/compiler/package.json

@@ -11,14 +11,11 @@
     "watch:ts": "tsc -watch"
   },
   "dependencies": {
-    "html-to-hyperscript": "^0.8.0",
     "pug": "^3.0.2",
     "pug-code-gen": "^2.0.3",
     "pug-error": "^2.0.0",
     "pug-linker": "^4.0.0",
     "pug-parser": "^6.0.0",
-    "snabbdom": "^3.5.0",
-    "snabbdom-virtualize": "^0.7.0",
     "ts-morph": "^15.1.0"
   },
   "types": "./types/",

+ 8 - 3
packages/compiler/src/core/Compiler.ts

@@ -7,7 +7,7 @@ import parse from "pug-parser";
 import link from "pug-linker";
 import codeGen from "pug-code-gen";
 import { Console } from "console";
-import { createWriteStream } from "fs";
+import { createWriteStream, existsSync, mkdirSync } from "fs";
 import { PugToVirtualDOM } from "./compiler/HTMLToVirtualDOM";
 
 export enum CompilationType {
@@ -58,7 +58,7 @@ export class PupperCompiler {
     /**
      * An exclusive console instance for debugging purposes.
      */
-    public debugger = new Console(createWriteStream(process.cwd() + "/.logs/log.log"), createWriteStream(process.cwd() + "/.logs/error.log"));
+    public debugger: Console;
 
     constructor(
         /**
@@ -66,7 +66,12 @@ export class PupperCompiler {
          */
         public options: ICompilerOptions
     ) {
-        
+        if (!existsSync(process.cwd() + "/.logs")) {
+            mkdirSync(process.cwd() + "/.logs", { recursive: true });
+        }
+
+        // Create the debug logger
+        this.debugger = new Console(createWriteStream(process.cwd() + "/.logs/log.log"), createWriteStream(process.cwd() + "/.logs/error.log"));
     }
 
     /**

+ 1 - 2
packages/renderer/package.json

@@ -11,15 +11,14 @@
     "watch:ts": "tsc -watch"
   },
   "dependencies": {
-    "@types/virtual-dom": "^2.1.1",
     "dom2hscript": "^0.2.3",
-    "morphdom": "^2.6.1",
     "pug": "^3.0.2",
     "virtual-dom": "^2.1.1"
   },
   "types": "./types/",
   "devDependencies": {
     "@types/node": "^16.7.6",
+    "@types/virtual-dom": "^2.1.1",
     "debug": "^4.3.4",
     "tsc": "^2.0.3",
     "typescript": "^4.4.2",

+ 62 - 14
packages/renderer/src/core/vdom/Node.ts

@@ -6,7 +6,7 @@ import VText from "virtual-dom/vnode/vtext";
 
 import h from "virtual-dom/h";
 import diff from "virtual-dom/diff";
-import { patch } from "virtual-dom";
+import { patch, VNode } from "virtual-dom";
 
 const debug = require("debug")("pupper:vdom:node");
 
@@ -29,11 +29,11 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
 
     private ignore: boolean = false;
     private dirty: boolean = true;
-    private patching: boolean;
+    private patching: boolean = false;
+    private renderable: boolean = true;
 
     public text: string = "";
     public element: Element = null;
-    key: string;
 
     constructor(
         protected node: TNode | string,
@@ -42,7 +42,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
     ) {
         if (typeof node !== "string") {
             // Initialize the properties
-            this.tag = "tagName" in node ? node.tagName : "text";
+            this.tag = "tagName" in node ? node.tagName : "TEXT";
 
             if ("properties" in node) {
                 if ("attrs" in node.properties) {
@@ -66,7 +66,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
                 this.children = node.children.map((child) => new PupperNode(child, this, renderer));
             }
         } else {
-            this.tag = "text";
+            this.tag = "TEXT";
             this.text = node;
         }
     }
@@ -96,23 +96,47 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
     /**
      * Sets all children to dirty.
      * @param dirty If it's dirty or not.
-     * @param autoPatch If can automatically call patch()
      * @returns 
      */
     public setChildrenDirty(dirty: boolean = true, autoPatch: boolean = true) {
         this.children.forEach((child) => {
+            child.setDirty(dirty, autoPatch);
             child.setChildrenDirty(dirty, autoPatch);
         });
 
         return this;
     }
 
+    /**
+     * Sets all children to dirty.
+     * @param ignored If it's dirty or not.
+     * @returns 
+     */
+    public setChildrenIgnored(ignored: boolean = true) {
+        this.children.forEach((child) => {
+            child.setIgnored(ignored);
+            child.setChildrenIgnored(ignored);
+        });
+
+        return this;
+    }
+
     /**
      * Determines if this node is being ignored by the directives.
      * @param ignored If this node needs to be ignored.
      */
     public setIgnored(ignored: boolean = true) {
         this.ignore = ignored;
+        return this;
+    }
+
+    /**
+     * Sets if this node can be rendered.
+     * @param renderable If this node can be rendered.
+     */
+    public setRenderable(renderable: boolean = true) {
+        this.renderable = renderable;
+        return this;
     }
 
     /**
@@ -123,6 +147,14 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
         return this.ignore || !this.dirty;
     }
 
+    /**
+     * Determines if this node can be rendered.
+     * @returns 
+     */
+    public isRenderable() {
+        return this.renderable;
+    }
+
     /**
      * Retrieves an object containing all attributes and properties.
      * @returns 
@@ -213,14 +245,15 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @param nodes The nodes to replace the current one.
      * @returns 
      */
-    public replaceWith(...nodes: (PupperNode | VirtualDOM.VTree)[]) {
+    public replaceWith<TNode extends PupperNode | VirtualDOM.VTree | string>(...nodes: TNode[]) {
         if (!this.parent) {
-            return false;
+            return nodes;
         }
 
         this.parent.children.splice(
             this.getIndex(),
             1,
+            // @ts-ignore
             ...nodes.map((node) => !(node instanceof PupperNode) ? new PupperNode(node, this.parent, this.renderer) : node)
         );
 
@@ -284,6 +317,12 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      */
     public setParent(parent: PupperNode) {
         this.parent = parent;
+
+        // Update the children parents
+        this.children.forEach((child) => {
+            child.setParent(this);
+        });
+
         return this;
     }
 
@@ -342,7 +381,16 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @returns 
      */
     public clone() {
-        return new PupperNode(cloneNode(this.node || this.text), this.parent, this.renderer);
+        const clonedNode = this.node === undefined ? this.text : h(this.tag, {
+            attrs: { ...this.attributes },
+            props: { ...this.properties },
+            on: {... this.eventListeners }
+        }, []);
+
+        const clone = new PupperNode(clonedNode, this.parent, this.renderer);
+        clone.children = this.children.map((child) => child.clone());
+
+        return clone;
     }
 
     /**
@@ -380,20 +428,20 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Patches the VDOM element in real DOM.
      */
     private doPatch() {
-        const diffs = diff(this.node as any, this.toVNode());
+        const diffs = diff(this.node as any, this.toVNode() as any);
 
         this.element = patch(this.element, diffs);
         this.patching = false;
+        this.dirty = false;
     }
 
     /**
      * Converts the current node into a virtual DOM node.
      * @returns 
      */
-    public toVNode(): VirtualDOM.VTree {
-        if (this.tag === "text") {
-            this.node = new VText(this.text) as TNode;
-            return this.node as VirtualDOM.VText;
+    public toVNode(): VirtualDOM.VTree | string {
+        if (typeof this.node === "string") {
+            return this.node;
         }
 
         const properties: Record<string, any> = {

+ 9 - 1
packages/renderer/src/core/vdom/directives/Conditional.ts

@@ -17,13 +17,21 @@ directive("if", async (node, { expression, scope }) => {
     const children = node.children;
     node = node.replaceWithComment();
     node.setIgnored();
+    node.setRenderable(false);
 
-    let clones: PupperNode[] = [];
+   let clones: PupperNode[] = [];
+   let lastValue: boolean = null;
 
     await effect(async () => {
         try {
             const value = await evaluate(scope);
 
+            if (lastValue === value) {
+                return;
+            }
+
+            lastValue = value;
+
             debug("%s evaluated to %O", expression, value);
 
             // If already rendered the clones

+ 16 - 12
packages/renderer/src/core/vdom/directives/Loop.ts

@@ -17,10 +17,11 @@ directive("for", async (node, { expression, scope }) => {
     const children = node.children;
     node = node.replaceWithComment();
     node.setIgnored();
+    node.setRenderable(false);
 
     let clones: PupperNode[] = [];
 
-    await effect(async () => {        
+    const removeEffect = await effect(async () => {        
         let loopScope;
 
         try {
@@ -42,25 +43,22 @@ directive("for", async (node, { expression, scope }) => {
 
             // Clear the older nodes if needed
             if (clones.length) {
-                node.parent.children.splice(
-                    node.getIndex() - clones.length,
-                    clones.length
-                );
-
+                clones.forEach((clone) => clone.delete());
                 clones = [];
             }
 
             // Iterate over all evaluated items
-            for(let item in items) {
+            for(let index = 0; index < items.length; index++) {
+                // Clone the scope
                 loopScope = { ...scope };
 
                 // Push the current item to the state stack
                 if ("item" in loopData) {
-                    loopScope[loopData.item] = items[item];
+                    loopScope[loopData.item] = items[index];
                 }
 
                 if ("index" in loopData) {
-                    loopScope[loopData.index] = item;
+                    loopScope[loopData.index] = index;
                 }
 
                 if ("collection" in loopData) {
@@ -68,14 +66,18 @@ directive("for", async (node, { expression, scope }) => {
                 }
 
                 for(let child of children) {
-                    child = child.clone().setParent(node.parent);                    
+                    child = child.clone()
+                        .setIgnored(false)
+                        .setParent(node.parent)
+                        .setDirty(true, false)
+                        .setChildrenDirty(true, false)
+                        .setChildrenIgnored(false);
+
                     node.insertBefore(child);
 
                     child = await walk(child, loopScope);
                     clones.push(child);
                 }
-
-                console.log(node, items[item]);
             }
 
             node.parent.setDirty();
@@ -85,6 +87,8 @@ directive("for", async (node, { expression, scope }) => {
             console.error(e);
         }
     });
+
+    node.addEventListener("removed", removeEffect);
 });
 
 /**

+ 10 - 11
packages/renderer/src/core/vdom/directives/Text.ts

@@ -6,6 +6,9 @@ import { PupperNode } from "../Node";
 /**
  * @directive x-text
  * @description Sets an element inner text.
+ * 
+ * @todo When a buffered text is given, no text is displayed.
+ *       Maybe the HTML to VDom is treating buffered text incorrectly.
  */
 directive("text", async (node, { expression, scope }) => {
     const evaluate = maybeEvaluateLater(expression);
@@ -21,18 +24,14 @@ directive("text", async (node, { expression, scope }) => {
                 return;
             }
 
-            if (node.tag === "text") {
-                node.replaceWith(new PupperNode(text, node.parent, node.renderer));
+            if (replacedNode) {
+                replacedNode = replacedNode.replaceWith(
+                    new PupperNode(text, node, node.renderer)
+                )[0];
             } else {
-                if (replacedNode) {
-                    replacedNode = replacedNode.replaceWith(
-                        new PupperNode(text, node, node.renderer)
-                    );
-                } else {
-                    replacedNode = new PupperNode(text, node, node.renderer);
-                    node.appendChild(replacedNode);
-                    node.removeAttribute("x-text");
-                }
+                replacedNode = new PupperNode(text, node, node.renderer);
+                node.appendChild(replacedNode);
+                node.removeAttribute("x-text");
             }
 
             node.setDirty();

+ 16 - 6
packages/renderer/src/model/NodeWalker.ts

@@ -38,24 +38,26 @@ export async function walk<TNode extends PupperNode | PupperNode[]>(nodes: TNode
 }
 
 async function node(node: PupperNode | undefined, scope: any) {
-    //console.group(node.tag, node.getAttributesAndProps());
+    console.group(node.tag, node.getAttributesAndProps(), node);
 
     // If it's an invalid node
     if (!node) {
-        //console.groupEnd();
+        console.groupEnd();
         // Ignore it
         return undefined;
     }
 
     // Ignore if it's a string
     if (typeof node === "string") {
-        //console.groupEnd();
+        console.info("node is a string");
+        console.groupEnd();
         return node;
     }
 
     // Ignore if it's being ignored
     if (node.isBeingIgnored()) {
-        //console.groupEnd();
+        console.info("node is being ignored");
+        console.groupEnd();
         return node;
     }
 
@@ -68,7 +70,8 @@ async function node(node: PupperNode | undefined, scope: any) {
 
     // If the node was removed, stop parsing
     if (!node.exists()) {
-        //console.groupEnd();
+        console.info("node was removed");
+        console.groupEnd();
         return node;
     }
 
@@ -77,7 +80,14 @@ async function node(node: PupperNode | undefined, scope: any) {
         node.children = await walk(node.children, scope);
     }
 
-    //console.groupEnd();
+    // If it's non-renderable
+    if (!node.isRenderable()) {
+        console.info("node is not renderable");
+        // Allow parsing its children but prevent itself from being rendered.
+        return undefined;
+    }
+
+    console.groupEnd();
 
     return node;
 }

+ 34 - 2
packages/renderer/src/model/Reactivity.ts

@@ -1,20 +1,48 @@
 type TEffect = () => any | Promise<any>;
 type TReactiveObj = Record<string | number | symbol, any>;
 
-const effects = new Map<TReactiveObj, TEffect | any>();
+const effects = new Map<TReactiveObj, Record<string | symbol, TEffect[]>>();
 let currentEffect: TEffect = null;
 
+const debug = require("debug")("pupper:reactivity");
+
+const ProxySymbol = Symbol("$Proxy");
+
 export async function effect(effect: TEffect) {
     currentEffect = effect;
 
+    debug("processing effect %O", effect);
+
     // Calling the effect immediately will make it
     // be detected and registered at the effects handler.
     await effect();
 
+    debug("effect was processed");
+
     currentEffect = null;
+
+    return () => {
+        effects.forEach((val, key) => {
+            for(let prop in val) {
+                if (val[prop].includes(effect)) {
+                    val[prop].splice(val[prop].indexOf(effect), 1);
+                    effects.set(key, val);
+                }
+            }
+        });
+    };
 }
 
 export function reactive(obj: TReactiveObj) {
+    for(let property in obj) {
+        // Proxy subobjects
+        if ((typeof obj[property] === "object" || Array.isArray(obj[property])) && obj[ProxySymbol] === undefined) {
+            obj[property] = reactive(obj[property]);
+        }
+    }
+
+    obj[ProxySymbol] = true;
+
     return new Proxy(obj, {
         get(target, property) {
             // If detected no current effect
@@ -49,6 +77,8 @@ export function reactive(obj: TReactiveObj) {
                 targetEffects[property].push(currentEffect);
             }
 
+            debug("effect access property %s from %O", property, target);
+
             return target[property];
         },
 
@@ -59,7 +89,9 @@ export function reactive(obj: TReactiveObj) {
             } else
             // Only objects can be reactive
             if (typeof value === "object") {
-                target[property] = reactive(value);
+                if (value[ProxySymbol] === undefined) {
+                    target[property] = reactive(value);
+                }
             } else {
                 target[property] = value;
             }

+ 2 - 2
packages/renderer/src/model/VirtualDom.ts

@@ -1,3 +1,4 @@
+import h from "virtual-dom/h";
 import VNode from "virtual-dom/vnode/vnode";
 import VText from "virtual-dom/vnode/vtext";
 
@@ -25,6 +26,5 @@ export function cloneNode(node: VirtualDOM.VTree | string): VirtualDOM.VTree {
         return new VText(node);
     }
 
-    // @ts-ignore
-    return new VNode(node.tagName, node.properties, node.children, node.key, node.namespace);
+    return h(node.tagName, node.properties, node.children);
 }

+ 3 - 3
test/templates/template.pupper

@@ -18,7 +18,7 @@ template
 
                     .row.mt-5.justify-content-around.align-items-center
                         if puppies === undefined || puppies.length === 0
-                            |Oh noe! No puppies to show :(
+                            ="Oh noe! No puppies to show :("
                         else
                             //- Render the puppies and share the onClickPuppy method with it
                             each index, puppy in puppies
@@ -28,13 +28,13 @@ template
 
                                         .card-header
                                             h5.card-title=puppy.title
-                                            small.text-muted|Served by pupper.js
+                                            small.text-muted="Served by pupper.js"
 
                                         .card-body
                                             !=puppy.description
 
                                             if puppy.shibe === true
-                                                p.text-warning|shibe!!!
+                                                p.text-warning="shibbe!!!"
 
                                             if puppy.properties
                                                 each property in puppy.properties