From 7f60010c1eec90e9b94f051cea39c3bede7f25a0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Feb 2024 18:52:21 -0800 Subject: [PATCH] set up a typescript pipeline for interpreter --- packages/interpreter/build.rs | 65 +++++++++++++++ packages/interpreter/gen/README.md | 3 + .../interpreter/src/{common.js => common.ts} | 1 + packages/interpreter/src/form.ts | 0 packages/interpreter/src/gen/common.js | 81 +++++++++++++++++++ packages/interpreter/src/gen/form.js | 1 + packages/interpreter/src/gen/interpreter.js | 0 .../src/{interpreter.js => interpreter.ts} | 81 ++++++++++--------- packages/interpreter/src/lib.rs | 6 +- packages/web/src/dom.rs | 2 - packages/web/src/event.rs | 1 + 11 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 packages/interpreter/build.rs create mode 100644 packages/interpreter/gen/README.md rename packages/interpreter/src/{common.js => common.ts} (99%) create mode 100644 packages/interpreter/src/form.ts create mode 100644 packages/interpreter/src/gen/common.js create mode 100644 packages/interpreter/src/gen/form.js create mode 100644 packages/interpreter/src/gen/interpreter.js rename packages/interpreter/src/{interpreter.js => interpreter.ts} (99%) diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs new file mode 100644 index 000000000..776f91307 --- /dev/null +++ b/packages/interpreter/build.rs @@ -0,0 +1,65 @@ +use std::{ + fs::read_to_string, + hash::{DefaultHasher, Hash, Hasher}, + process::Command, +}; + +fn main() { + // If any TS changes, re-run the build script + println!("cargo:rerun-if-changed=src/*.ts"); + + for entry in ["common", "form", "interpreter"].iter() { + gen_bindings(entry); + } +} + +// okay...... so tsc might fail if the user doesn't have it installed +// we don't really want to fail if that's the case +// but if you started *editing* the .ts files, you're gonna have a bad time +// so..... +// we need to hash each of the .ts files and add that hash to the JS files +// if the hashes don't match, we need to fail the build +// that way we also don't need +fn gen_bindings(name: &str) { + let contents = read_to_string(&format!("src/{name}.ts")).unwrap(); + let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default(); + let hashed = hash_file(&contents); + + // If the file is generated, and the hash is the same, we're good, don't do anything + if generated + .lines() + .next() + .unwrap_or_default() + .starts_with(&format!("// DO NOT EDIT THIS FILE. HASH: {}", hashed)) + { + return; + } + + // If the file is generated, and the hash is different, we need to generate it + let status = Command::new("tsc") + .arg(format!("src/{name}.ts")) + .arg("--outDir") + .arg("gen") + .arg("--target") + .arg("es6") + .status() + .unwrap(); + + if !status.success() { + panic!( + "Failed to generate bindings for {}. Make sure you have tsc installed", + name + ); + } + + // The file should exist, and now we need write the TS hash to the file + let generated = read_to_string(&format!("gen/{name}.js")).unwrap(); + let generated = format!("// DO NOT EDIT THIS FILE. HASH: {}\n{}", hashed, generated); + std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap(); +} + +fn hash_file(obj: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + obj.hash(&mut hasher); + hasher.finish() +} diff --git a/packages/interpreter/gen/README.md b/packages/interpreter/gen/README.md new file mode 100644 index 000000000..e6d41cee9 --- /dev/null +++ b/packages/interpreter/gen/README.md @@ -0,0 +1,3 @@ +temporary generated code directory since tsc doesn't have a crossplatform way of generating typescript to stdout + +https://github.com/microsoft/TypeScript/issues/1226#issuecomment-523544134 diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.ts similarity index 99% rename from packages/interpreter/src/common.js rename to packages/interpreter/src/common.ts index 0c64085f9..ee16838ee 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.ts @@ -1,4 +1,5 @@ export function setAttributeInner(node, field, value, ns) { + const name = field; if (ns === "style") { // ????? why do we need to do this diff --git a/packages/interpreter/src/form.ts b/packages/interpreter/src/form.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/interpreter/src/gen/common.js b/packages/interpreter/src/gen/common.js new file mode 100644 index 000000000..8f21059f3 --- /dev/null +++ b/packages/interpreter/src/gen/common.js @@ -0,0 +1,81 @@ +// DO NOT EDIT THIS FILE. HASH: 9578489549991746027 +export function setAttributeInner(node, field, value, ns) { + const name = field; + if (ns === "style") { + // ????? why do we need to do this + if (node.style === undefined) { + node.style = {}; + } + node.style[name] = value; + } + else if (!!ns) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value !== node.value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } +} +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, + webkitdirectory: true, +}; +function truthy(val) { + return val === "true" || val === true; +} diff --git a/packages/interpreter/src/gen/form.js b/packages/interpreter/src/gen/form.js new file mode 100644 index 000000000..0357a8736 --- /dev/null +++ b/packages/interpreter/src/gen/form.js @@ -0,0 +1 @@ +// DO NOT EDIT THIS FILE. HASH: 3476900567878811119 diff --git a/packages/interpreter/src/gen/interpreter.js b/packages/interpreter/src/gen/interpreter.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.ts similarity index 99% rename from packages/interpreter/src/interpreter.js rename to packages/interpreter/src/interpreter.ts index 977df8ca2..c8dcef095 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.ts @@ -1,5 +1,7 @@ class InterpreterConfig { - constructor(intercept_link_redirects) { + intercept_link_redirects: boolean; + + constructor(intercept_link_redirects: boolean) { this.intercept_link_redirects = intercept_link_redirects; } } @@ -21,6 +23,42 @@ async function handler(event, name, bubbles, config) { let contents = await serialize_event(event); + if ( + target.tagName === "FORM" && + (event.type === "submit" || event.type === "input") + ) { + const formData = new FormData(target); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + } + } + + if ( + target.tagName === "SELECT" && + event.type === "input" + ) { + const selectData = target.options; + contents.values["options"] = []; + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents.values["options"].push(option.value.toString()); + } + } + } + // TODO: this should be liveview only if ( target.tagName === "INPUT" && @@ -61,42 +99,6 @@ async function handler(event, name, bubbles, config) { } } - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - const formData = new FormData(target); - - for (let name of formData.keys()) { - const fieldType = target.elements[name].type; - - switch (fieldType) { - case "select-multiple": - contents.values[name] = formData.getAll(name); - break; - - // add cases for fieldTypes that can hold multiple values here - default: - contents.values[name] = formData.get(name); - break; - } - } - } - - if ( - target.tagName === "SELECT" && - event.type === "input" - ) { - const selectData = target.options; - contents.values["options"] = []; - for (let i = 0; i < selectData.length; i++) { - let option = selectData[i]; - if (option.selected) { - contents.values["options"].push(option.value.toString()); - } - } - } - window.ipc.postMessage( window.interpreter.serializeIpcMessage("user_event", { name: name, @@ -324,7 +326,7 @@ window.interpreter.setFocus = function (id, focus) { return true; } -function get_mouse_data(event) { +function get_mouse_data(event: MouseEvent) { const { altKey, button, @@ -447,7 +449,8 @@ async function serialize_event(event) { case "dragover": case "dragstart": case "drop": { - let files = null; + let files = []; + if (event.dataTransfer && event.dataTransfer.files) { files = ["a", "b", "c"]; // files = await serializeFileList(event.dataTransfer.files); diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index b1aaebf04..d19ccb585 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -2,8 +2,8 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); -pub static COMMON_JS: &str = include_str!("./common.js"); +pub static INTERPRETER_JS: &str = include_str!("./gen/interpreter.js"); +pub static COMMON_JS: &str = include_str!("./gen/common.js"); #[cfg(feature = "sledgehammer")] mod sledgehammer_bindings; @@ -20,7 +20,7 @@ pub use write_native_mutations::*; #[cfg(all(feature = "minimal_bindings", feature = "webonly"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - #[wasm_bindgen(module = "/src/common.js")] + #[wasm_bindgen(module = "/src/gen/common.js")] extern "C" { pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 33f44c1d8..51aba110b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -36,8 +36,6 @@ pub struct UiEvent { pub data: PlatformEventData, } -//fn get_document(elem: &web_sys::Element) -> - impl WebsysDom { pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { let (document, root) = match cfg.root { diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index 8fa2b1aea..f68487925 100644 --- a/packages/web/src/event.rs +++ b/packages/web/src/event.rs @@ -441,6 +441,7 @@ impl HasFileData for WebFormData { fn files(&self) -> Option> { #[cfg(not(feature = "file_engine"))] let files = None; + #[cfg(feature = "file_engine")] let files = self .element