dont even hash it!

This commit is contained in:
Jonathan Kelley 2024-03-06 18:59:15 -08:00
parent c75aa48c74
commit a2907d138d
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
2 changed files with 560 additions and 4 deletions

View file

@ -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

View file

@ -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);
}
}