Browse Source

dont even hash it!

Jonathan Kelley 1 year ago
parent
commit
a2907d138d
2 changed files with 560 additions and 4 deletions
  1. 3 3
      packages/interpreter/build.rs
  2. 557 1
      packages/interpreter/src/js/hash.txt

+ 3 - 3
packages/interpreter/build.rs

@@ -13,7 +13,7 @@ fn main() {
         return;
     }
 
-    panic!("Hashes match, no need to update bindings. {expected} != {hash}",);
+    // panic!("Hashes match, no need to update bindings. {expected} != {hash}",);
 
     // Otherwise, generate the bindings and write the new hash to disk
     // Generate the bindings for both native and web
@@ -21,7 +21,7 @@ fn main() {
     gen_bindings("native", "native");
     gen_bindings("core", "core");
 
-    std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
+    std::fs::write("src/js/hash.txt", hash).unwrap();
 }
 
 /// Hashes the contents of a directory
@@ -35,7 +35,7 @@ fn hash_ts_files() -> String {
     ];
 
     for file in files {
-        out = format!("{out}{:?}", md5::compute(file));
+        out = format!("{out}{file}");
     }
 
     out

+ 557 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1,557 @@
-3bd5a1310ebd728bdedb486881d5ad99151cf07b0006de52240dfd757ea3353e90df122a7ae681f33ff620cf15c47a20
+export { setAttributeInner } from "./set_attribute";
+export { retrieveFormValues } from "./form";
+// This file provides an extended variant of the interpreter used for desktop and liveview interaction
+//
+// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't
+// provide since it doesn't have access to the dom.
+
+import { BaseInterpreter, NodeId } from "./core";
+import { SerializedEvent, serializeEvent } from "./serialize";
+
+// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope
+// we want to extend it, and it technically extends base intepreter. To make typescript happy,
+// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it
+var JSChannel_: typeof BaseInterpreter;
+
+// @ts-ignore - this is coming from the host
+if (RawInterpreter !== undefined && RawInterpreter !== null) {
+  // @ts-ignore - this is coming from the host
+  JSChannel_ = RawInterpreter;
+};
+
+export class NativeInterpreter extends JSChannel_ {
+  intercept_link_redirects: boolean;
+  ipc: any;
+  editsPath: string;
+
+  // eventually we want to remove liveview and build it into the server-side-events of fullstack
+  // however, for now we need to support it since SSE in fullstack doesn't exist yet
+  liveview: boolean;
+
+  constructor(editsPath: string) {
+    super();
+    this.editsPath = editsPath;
+  }
+
+  initialize(root: HTMLElement): void {
+    this.intercept_link_redirects = true;
+    this.liveview = false;
+
+    // attach an event listener on the body that prevents file drops from navigating
+    // this is because the browser will try to navigate to the file if it's dropped on the window
+    window.addEventListener("dragover", function (e) {
+      // // check which element is our target
+      if (e.target instanceof Element && e.target.tagName != "INPUT") {
+        e.preventDefault();
+      }
+    }, false);
+
+    window.addEventListener("drop", function (e) {
+      let target = e.target;
+
+      if (!(target instanceof Element)) {
+        return;
+      }
+
+      // Dropping a file on the window will navigate to the file, which we don't want
+      e.preventDefault();
+    }, false);
+
+    // attach a listener to the route that listens for clicks and prevents the default file dialog
+    window.addEventListener("click", (event) => {
+      const target = event.target;
+      if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") {
+        // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
+        let target_id = getTargetId(target);
+        if (target_id !== null) {
+          const message = this.serializeIpcMessage("file_dialog", {
+            event: "change&input",
+            accept: target.getAttribute("accept"),
+            directory: target.getAttribute("webkitdirectory") === "true",
+            multiple: target.hasAttribute("multiple"),
+            target: target_id,
+            bubbles: event.bubbles,
+          });
+          this.ipc.postMessage(message);
+        }
+
+        // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate
+        event.preventDefault();
+      }
+    });
+
+
+    // @ts-ignore - wry gives us this
+    this.ipc = window.ipc;
+
+    // make sure we pass the handler to the base interpreter
+    const handler: EventListener = (event) => this.handleEvent(event, event.type, true);
+    super.initialize(root, handler);
+  }
+
+  serializeIpcMessage(method: string, params = {}) {
+    return JSON.stringify({ method, params });
+  }
+
+  scrollTo(id: NodeId, behavior: ScrollBehavior) {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      node.scrollIntoView({ behavior });
+    }
+  }
+
+  getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      const rect = node.getBoundingClientRect();
+      return {
+        type: "GetClientRect",
+        origin: [rect.x, rect.y],
+        size: [rect.width, rect.height],
+      };
+    }
+  }
+
+  setFocus(id: NodeId, focus: boolean) {
+    const node = this.nodes[id];
+
+    if (node instanceof HTMLElement) {
+      if (focus) {
+        node.focus();
+      } else {
+        node.blur();
+      }
+    }
+  }
+
+  // ignore the fact the base interpreter uses ptr + len but we use array...
+  // @ts-ignore
+  loadChild(array: number[]) {
+    // iterate through each number and get that child
+    let node = this.stack[this.stack.length - 1];
+
+    for (let i = 0; i < array.length; i++) {
+      let end = array[i];
+      for (node = node.firstChild; end > 0; end--) {
+        node = node.nextSibling;
+      }
+    }
+
+    return node;
+  }
+
+  appendChildren(id: NodeId, many: number) {
+    const root = this.nodes[id];
+    const els = this.stack.splice(this.stack.length - many);
+
+    for (let k = 0; k < many; k++) {
+      root.appendChild(els[k]);
+    }
+  }
+
+  handleEvent(event: Event, name: string, bubbles: boolean) {
+    const target = event.target!;
+    const realId = getTargetId(target)!;
+    const contents = serializeEvent(event, target);
+
+    // Handle the event on the virtualdom and then preventDefault if it also preventsDefault
+    // Some listeners
+    let body = {
+      name: name,
+      data: contents,
+      element: realId,
+      bubbles,
+    };
+
+    // Run any prevent defaults the user might've set
+    // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary
+    // now that we expose preventDefault to the virtualdom on desktop
+    // Liveview will still need to use this
+    this.preventDefaults(event, target);
+
+    // liveview does not have syncronous event handling, so we need to send the event to the host
+    if (this.liveview) {
+      // Okay, so the user might've requested some files to be read
+      if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) {
+        if (target.getAttribute("type") === "file") {
+          this.readFiles(target, contents, bubbles, realId, name);
+        }
+      }
+    } else {
+
+      const message = this.serializeIpcMessage("user_event", body);
+      this.ipc.postMessage(message);
+
+      // // Run the event handler on the virtualdom
+      // // capture/prevent default of the event if the virtualdom wants to
+      // const res = handleVirtualdomEventSync(JSON.stringify(body));
+
+      // if (res.preventDefault) {
+      //   event.preventDefault();
+      // }
+
+      // if (res.stopPropagation) {
+      //   event.stopPropagation();
+      // }
+    }
+  }
+
+
+
+  // This should:
+  // - prevent form submissions from navigating
+  // - prevent anchor tags from navigating
+  // - prevent buttons from submitting forms
+  // - let the virtualdom attempt to prevent the event
+  preventDefaults(event: Event, target: EventTarget) {
+    let preventDefaultRequests: string | null = null;
+
+    // Some events can be triggered on text nodes, which don't have attributes
+    if (target instanceof Element) {
+      preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
+    }
+
+    if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) {
+      event.preventDefault();
+    }
+
+    if (event.type === "submit") {
+      event.preventDefault();
+    }
+
+    // Attempt to intercept if the event is a click
+    if (target instanceof Element && event.type === "click") {
+      this.handleClickNavigate(event, target, preventDefaultRequests);
+    }
+  }
+
+  handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) {
+    // todo call prevent default if it's the right type of event
+    if (!this.intercept_link_redirects) {
+      return;
+    }
+
+    // prevent buttons in forms from submitting the form
+    if (target.tagName === "BUTTON" && event.type == "submit") {
+      event.preventDefault();
+    }
+
+    // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating
+    let a_element = target.closest("a");
+    if (a_element == null) {
+      return;
+    }
+
+    event.preventDefault();
+
+    let elementShouldPreventDefault =
+      preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
+
+    let aElementShouldPreventDefault = a_element.getAttribute(
+      `dioxus-prevent-default`
+    );
+
+    let linkShouldPreventDefault =
+      aElementShouldPreventDefault &&
+      aElementShouldPreventDefault.includes(`onclick`);
+
+    if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
+      const href = a_element.getAttribute("href");
+      if (href !== "" && href !== null && href !== undefined) {
+        this.ipc.postMessage(
+          this.serializeIpcMessage("browser_open", { href })
+        );
+      }
+    }
+  }
+
+  waitForRequest(headless: boolean) {
+    fetch(new Request(this.editsPath))
+      .then(response => response.arrayBuffer())
+      .then(bytes => {
+        // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
+        if (headless) {
+          // @ts-ignore
+          this.run_from_bytes(bytes);
+        } else {
+          // @ts-ignore
+          requestAnimationFrame(() => this.run_from_bytes(bytes));
+        }
+        this.waitForRequest(headless);
+      });
+  }
+
+
+  //  A liveview only function
+  // Desktop will intercept the event before it hits this
+  async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {
+    let files = target.files!;
+    let file_contents: { [name: string]: number[] } = {};
+
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+      file_contents[file.name] = Array.from(
+        new Uint8Array(await file.arrayBuffer())
+      );
+    }
+
+    contents.files = { files: file_contents };
+
+    const message = this.serializeIpcMessage("user_event", {
+      name: name,
+      element: realId,
+      data: contents,
+      bubbles,
+    });
+
+    this.ipc.postMessage(message);
+  }
+}
+
+type EventSyncResult = {
+  preventDefault: boolean;
+  stopPropagation: boolean;
+  stopImmediatePropagation: boolean;
+  filesRequested: boolean;
+};
+
+// This function sends the event to the virtualdom and then waits for the virtualdom to process it
+//
+// However, it's not really suitable for liveview, because it's synchronous and will block the main thread
+// We should definitely consider using a websocket if we want to block... or just not block on liveview
+// Liveview is a little bit of a tricky beast
+function handleVirtualdomEventSync(contents: string): EventSyncResult {
+  // Handle the event on the virtualdom and then process whatever its output was
+  const xhr = new XMLHttpRequest();
+
+  // Serialize the event and send it to the custom protocol in the Rust side of things
+  xhr.timeout = 1000;
+  xhr.open("GET", "/handle/event.please", false);
+  xhr.setRequestHeader("Content-Type", "application/json");
+  xhr.send(contents);
+
+  // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to
+  return JSON.parse(xhr.responseText);
+}
+
+function getTargetId(target: EventTarget): NodeId | null {
+  // Ensure that the target is a node, sometimes it's nota
+  if (!(target instanceof Node)) {
+    return null;
+  }
+
+  let ourTarget = target;
+  let realId = null;
+
+  while (realId == null) {
+    if (ourTarget === null) {
+      return null;
+    }
+
+    if (ourTarget instanceof Element) {
+      realId = ourTarget.getAttribute(`data-dioxus-id`);
+    }
+
+    ourTarget = ourTarget.parentNode;
+  }
+
+  return parseInt(realId);
+}
+
+
+// function applyFileUpload() {
+//   let inputs = document.querySelectorAll("input");
+//   for (let input of inputs) {
+//     if (!input.getAttribute("data-dioxus-file-listener")) {
+//       // prevent file inputs from opening the file dialog on click
+//       const type = input.getAttribute("type");
+//       if (type === "file") {
+//         input.setAttribute("data-dioxus-file-listener", true);
+//         input.addEventListener("click", (event) => {
+//           let target = event.target;
+//           let target_id = find_real_id(target);
+//           if (target_id !== null) {
+//             const send = (event_name) => {
+//               const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
+//               window.ipc.postMessage(message);
+//             };
+//             send("change&input");
+//           }
+//           event.preventDefault();
+//         });
+//       }
+//     }
+// }
+// The root interpreter class that holds state about the mapping between DOM and VirtualDom
+// This always lives in the JS side of things, and is extended by the native and web interpreters
+
+import { setAttributeInner } from "./set_attribute";
+
+export type NodeId = number;
+
+export class BaseInterpreter {
+  // non bubbling events listen at the element the listener was created at
+  global: {
+    [key: string]: { active: number, callback: EventListener }
+  };
+  // bubbling events can listen at the root element
+  local: {
+    [key: string]: {
+      [key: string]: EventListener
+    }
+  };
+
+  root: HTMLElement;
+  handler: EventListener;
+  nodes: Node[];
+  stack: Node[];
+  templates: {
+    [key: number]: Node[]
+  };
+
+  // sledgehammer is generating this...
+  m: any;
+
+  constructor() { }
+
+  initialize(root: HTMLElement, handler: EventListener | null = null) {
+    this.global = {};
+    this.local = {};
+    this.root = root;
+
+    this.nodes = [root];
+    this.stack = [root];
+    this.templates = {};
+
+    if (handler) {
+      this.handler = handler;
+    }
+  }
+
+  createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
+    if (bubbles) {
+      if (this.global[event_name] === undefined) {
+        this.global[event_name] = { active: 1, callback: this.handler };
+        this.root.addEventListener(event_name, this.handler);
+      } else {
+        this.global[event_name].active++;
+      }
+    } else {
+      const id = element.getAttribute("data-dioxus-id");
+      if (!this.local[id]) {
+        this.local[id] = {};
+      }
+      element.addEventListener(event_name, this.handler);
+    }
+  }
+
+  removeListener(element: HTMLElement, event_name: string, bubbles: boolean) {
+    if (bubbles) {
+      this.removeBubblingListener(event_name);
+    } else {
+      this.removeNonBubblingListener(element, event_name);
+    }
+  }
+
+  removeBubblingListener(event_name: string) {
+    this.global[event_name].active--;
+    if (this.global[event_name].active === 0) {
+      this.root.removeEventListener(event_name, this.global[event_name].callback);
+      delete this.global[event_name];
+    }
+  }
+
+  removeNonBubblingListener(element: HTMLElement, event_name: string) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id][event_name];
+    if (Object.keys(this.local[id]).length === 0) {
+      delete this.local[id];
+    }
+    element.removeEventListener(event_name, this.handler);
+  }
+
+  removeAllNonBubblingListeners(element: HTMLElement) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+
+  getNode(id: NodeId): Node {
+    return this.nodes[id];
+  }
+
+  appendChildren(id: NodeId, many: number) {
+    const root = this.nodes[id];
+    const els = this.stack.splice(this.stack.length - many);
+    for (let k = 0; k < many; k++) {
+      root.appendChild(els[k]);
+    }
+  }
+
+  loadChild(ptr: number, len: number): Node {
+    // iterate through each number and get that child
+    let node = this.stack[this.stack.length - 1] as Node;
+    let ptr_end = ptr + len;
+
+    for (; ptr < ptr_end; ptr++) {
+      let end = this.m.getUint8(ptr);
+      for (node = node.firstChild; end > 0; end--) {
+        node = node.nextSibling;
+      }
+    }
+
+    return node;
+  }
+
+  saveTemplate(nodes: HTMLElement[], tmpl_id: number) {
+    this.templates[tmpl_id] = nodes;
+  }
+
+  hydrate(ids: { [key: number]: number }) {
+    const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
+
+    for (let i = 0; i < hydrateNodes.length; i++) {
+      const hydrateNode = hydrateNodes[i] as HTMLElement;
+      const hydration = hydrateNode.getAttribute('data-node-hydration');
+      const split = hydration!.split(',');
+      const id = ids[parseInt(split[0])];
+
+      this.nodes[id] = hydrateNode;
+
+      if (split.length > 1) {
+        // @ts-ignore
+        hydrateNode.listening = split.length - 1;
+        hydrateNode.setAttribute('data-dioxus-id', id.toString());
+        for (let j = 1; j < split.length; j++) {
+          const listener = split[j];
+          const split2 = listener.split(':');
+          const event_name = split2[0];
+          const bubbles = split2[1] === '1';
+          this.createListener(event_name, hydrateNode, bubbles);
+        }
+      }
+    }
+
+    const treeWalker = document.createTreeWalker(
+      document.body,
+      NodeFilter.SHOW_COMMENT,
+    );
+
+    let currentNode = treeWalker.nextNode();
+
+    while (currentNode) {
+      const id = currentNode.textContent!;
+      const split = id.split('node-id');
+
+      if (split.length > 1) {
+        this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
+      }
+
+      currentNode = treeWalker.nextNode();
+    }
+  }
+
+  setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
+    setAttributeInner(node, field, value, ns);
+  }
+}
+