switch to a Document trait and introduce Script/Head/Style/Meta components (#2635)
* switch to a Document trait and introduce Script/Head/Style/Meta components * Fix desktop title * Insert special elements into the head during the inital SSR render * Make all head component attributes optional * hydrate head elements * improve the server streaming head warning * Document fullstack head hydration approach * deduplicate head elements by href * move Link into head::Link * document head components * add meta and title examples * Fix a few doc examples * fix formatting * Add title to playwright tests * serde is optional on web, but it is enabled by hydrate * remove leftover console log
This commit is contained in:
1 parent
a490a94ae5
commit
176e67e5b7
65 files changed
+1703
-495
No files matched your search
@@ -26,7 +26,7 @@ dioxus-core = { workspace = true, optional = true }
|
||||
dioxus-html = { workspace = true, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
md5 = "0.7.0"
|
||||
lazy-js-bundle = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -42,4 +42,3 @@ webonly = [
|
||||
]
|
||||
binary-protocol = ["sledgehammer", "dep:dioxus-core", "dep:dioxus-html"]
|
||||
minimal_bindings = []
|
||||
eval = []
|
||||
@@ -1,84 +1,14 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::path::PathBuf;
|
||||
use std::{hash::Hasher, process::Command};
|
||||
|
||||
fn main() {
|
||||
// If any TS changes, re-run the build script
|
||||
let watching = std::fs::read_dir("./src/ts").unwrap();
|
||||
let ts_paths: Vec<_> = watching
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().map(|ext| ext == "ts").unwrap_or(false))
|
||||
.collect();
|
||||
for path in &ts_paths {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
}
|
||||
|
||||
// Compute the hash of the ts files
|
||||
let hash = hash_ts_files(ts_paths);
|
||||
|
||||
// If the hash matches the one on disk, we're good and don't need to update bindings
|
||||
let fs_hash_string = std::fs::read_to_string("src/js/hash.txt");
|
||||
let expected = fs_hash_string
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.unwrap_or_default();
|
||||
if expected == hash.to_string() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, generate the bindings and write the new hash to disk
|
||||
// Generate the bindings for both native and web
|
||||
gen_bindings("common", "common");
|
||||
gen_bindings("native", "native");
|
||||
gen_bindings("core", "core");
|
||||
gen_bindings("eval", "eval");
|
||||
gen_bindings("native_eval", "native_eval");
|
||||
gen_bindings("hydrate", "hydrate");
|
||||
gen_bindings("initialize_streaming", "initialize_streaming");
|
||||
|
||||
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
|
||||
}
|
||||
|
||||
/// Hashes the contents of a directory
|
||||
fn hash_ts_files(mut files: Vec<PathBuf>) -> u64 {
|
||||
// Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
|
||||
files.sort();
|
||||
let mut hash = DefaultHasher::new();
|
||||
for file in files {
|
||||
let contents = std::fs::read_to_string(file).unwrap();
|
||||
// windows + git does a weird thing with line endings, so we need to normalize them
|
||||
for line in contents.lines() {
|
||||
hash.write(line.as_bytes());
|
||||
}
|
||||
}
|
||||
hash.finish()
|
||||
}
|
||||
|
||||
// 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(input_name: &str, output_name: &str) {
|
||||
// If the file is generated, and the hash is different, we need to generate it
|
||||
let status = Command::new("bun")
|
||||
.arg("build")
|
||||
.arg(format!("src/ts/{input_name}.ts"))
|
||||
.arg("--outfile")
|
||||
.arg(format!("src/js/{output_name}.js"))
|
||||
.arg("--minify-whitespace")
|
||||
.arg("--minify-syntax")
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
if !status.success() {
|
||||
panic!(
|
||||
"Failed to generate bindings for {}. Make sure you have tsc installed",
|
||||
input_name
|
||||
);
|
||||
}
|
||||
// If any TS files change, re-run the build script
|
||||
lazy_js_bundle::LazyTypeScriptBindings::new()
|
||||
.with_watching("./src/ts")
|
||||
.with_binding("./src/ts/common.ts", "./src/js/common.js")
|
||||
.with_binding("./src/ts/native.ts", "./src/js/native.js")
|
||||
.with_binding("./src/ts/core.ts", "./src/js/core.js")
|
||||
.with_binding("./src/ts/hydrate.ts", "./src/js/hydrate.js")
|
||||
.with_binding(
|
||||
"./src/ts/initialize_streaming.ts",
|
||||
"./src/js/initialize_streaming.js",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/// Code for the Dioxus channel used to communicate between the dioxus and javascript code
|
||||
pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js");
|
||||
|
||||
#[cfg(feature = "webonly")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct JSOwner {
|
||||
_owner: Box<dyn std::any::Any>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "webonly")]
|
||||
impl JSOwner {
|
||||
pub fn new(owner: impl std::any::Any) -> Self {
|
||||
Self {
|
||||
_owner: Box::new(owner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webonly")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
|
||||
extern "C" {
|
||||
pub type WebDioxusChannel;
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(owner: JSOwner) -> WebDioxusChannel;
|
||||
|
||||
#[wasm_bindgen(method, js_name = "rustSend")]
|
||||
pub fn rust_send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
|
||||
|
||||
#[wasm_bindgen(method, js_name = "rustRecv")]
|
||||
pub async fn rust_recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn weak(this: &WebDioxusChannel) -> WeakDioxusChannel;
|
||||
|
||||
pub type WeakDioxusChannel;
|
||||
|
||||
#[wasm_bindgen(method, js_name = "rustSend")]
|
||||
pub fn rust_send(this: &WeakDioxusChannel, value: wasm_bindgen::JsValue);
|
||||
|
||||
#[wasm_bindgen(method, js_name = "rustRecv")]
|
||||
pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
this files are generated - do not edit them!
|
||||
@@ -1 +0,0 @@
|
||||
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class WebDioxusChannel extends DioxusChannel{js_to_rust;rust_to_js;owner;constructor(owner){super();this.owner=owner,this.js_to_rust=new Channel,this.rust_to_js=new Channel}weak(){return new WeakDioxusChannel(this)}async recv(){return await this.rust_to_js.recv()}send(data){this.js_to_rust.send(data)}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){return await this.js_to_rust.recv()}}export{WebDioxusChannel,WeakDioxusChannel,DioxusChannel,Channel};
|
||||
@@ -1 +1 @@
|
||||
6628058622237003340
|
||||
[6449103750905854967, 12029349297046688094, 14626980229647476238, 8716623267269178440, 5336385715226370016, 14456089431355876478, 3589298972260118311, 2745859031945642653, 5638004933879392817]
|
||||
@@ -1 +0,0 @@
|
||||
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class QueryParams{id;data;constructor(id,method,data){this.id=id,this.data={method,data}}}window.__msg_queues=window.__msg_queues||[];window.finalizationRegistry=window.finalizationRegistry||new FinalizationRegistry(({id})=>{window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(id,"drop")}))});window.getQuery=function(request_id){return window.__msg_queues[request_id]};window.createQuery=function(request_id){return new NativeDioxusChannel(request_id)};class NativeDioxusChannel extends DioxusChannel{rust_to_js;request_id;constructor(request_id){super();this.rust_to_js=new Channel,this.request_id=request_id,window.__msg_queues[request_id]=this.weak(),window.finalizationRegistry.register(this,{id:request_id})}async recv(){return await this.rust_to_js.recv()}send(data){window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(this.request_id,"send",data)}))}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){}}export{NativeDioxusChannel};
|
||||
@@ -24,9 +24,6 @@ pub mod unified_bindings;
|
||||
#[cfg(feature = "sledgehammer")]
|
||||
pub use unified_bindings::*;
|
||||
|
||||
#[cfg(feature = "eval")]
|
||||
pub mod eval;
|
||||
|
||||
// Common bindings for minimal usage.
|
||||
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
|
||||
pub mod minimal_bindings {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// Handle communication between rust and evaluating javascript
|
||||
|
||||
export class Channel {
|
||||
pending: any[];
|
||||
waiting: ((data: any) => void)[];
|
||||
|
||||
constructor() {
|
||||
this.pending = [];
|
||||
this.waiting = [];
|
||||
}
|
||||
|
||||
send(data: any) {
|
||||
// If there's a waiting callback, call it
|
||||
if (this.waiting.length > 0) {
|
||||
this.waiting.shift()(data);
|
||||
return;
|
||||
}
|
||||
// Otherwise queue the data
|
||||
this.pending.push(data);
|
||||
}
|
||||
|
||||
async recv(): Promise<any> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
// If data already exists, resolve immediately
|
||||
if (this.pending.length > 0) {
|
||||
resolve(this.pending.shift());
|
||||
return;
|
||||
}
|
||||
// Otherwise queue the resolve callback
|
||||
this.waiting.push(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WeakDioxusChannel {
|
||||
inner: WeakRef<DioxusChannel>;
|
||||
|
||||
constructor(channel: DioxusChannel) {
|
||||
this.inner = new WeakRef(channel);
|
||||
}
|
||||
|
||||
// Send data from rust to javascript
|
||||
rustSend(data: any) {
|
||||
let channel = this.inner.deref();
|
||||
if (channel) {
|
||||
channel.rustSend(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Receive data sent from javascript in rust
|
||||
async rustRecv(): Promise<any> {
|
||||
let channel = this.inner.deref();
|
||||
if (channel) {
|
||||
return await channel.rustRecv();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class DioxusChannel {
|
||||
// Return a weak reference to this channel
|
||||
weak(): WeakDioxusChannel {
|
||||
return new WeakDioxusChannel(this);
|
||||
}
|
||||
|
||||
// Send data from rust to javascript
|
||||
abstract rustSend(data: any): void;
|
||||
|
||||
// Receive data sent from javascript in rust
|
||||
abstract rustRecv(): Promise<any>;
|
||||
}
|
||||
|
||||
export class WebDioxusChannel extends DioxusChannel {
|
||||
js_to_rust: Channel;
|
||||
rust_to_js: Channel;
|
||||
owner: any;
|
||||
|
||||
constructor(owner: any) {
|
||||
super();
|
||||
this.owner = owner;
|
||||
this.js_to_rust = new Channel();
|
||||
this.rust_to_js = new Channel();
|
||||
}
|
||||
|
||||
// Return a weak reference to this channel
|
||||
weak(): WeakDioxusChannel {
|
||||
return new WeakDioxusChannel(this);
|
||||
}
|
||||
|
||||
// Receive message from Rust
|
||||
async recv() {
|
||||
return await this.rust_to_js.recv();
|
||||
}
|
||||
|
||||
// Send message to rust.
|
||||
send(data: any) {
|
||||
this.js_to_rust.send(data);
|
||||
}
|
||||
|
||||
// Send data from rust to javascript
|
||||
rustSend(data: any) {
|
||||
this.rust_to_js.send(data);
|
||||
}
|
||||
|
||||
// Receive data sent from javascript in rust
|
||||
async rustRecv(): Promise<any> {
|
||||
return await this.js_to_rust.recv();
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Channel, DioxusChannel, WeakDioxusChannel } from "./eval";
|
||||
|
||||
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
|
||||
declare global {
|
||||
interface Window {
|
||||
__msg_queues: WeakDioxusChannel[];
|
||||
finalizationRegistry: FinalizationRegistry<{ id: number }>;
|
||||
|
||||
getQuery(request_id: number): WeakDioxusChannel;
|
||||
|
||||
createQuery(request_id: number): NativeDioxusChannel;
|
||||
}
|
||||
}
|
||||
|
||||
// A message that can be sent to the desktop renderer about a query
|
||||
class QueryParams {
|
||||
id: number;
|
||||
data: { method: "drop" | "send"; data?: any };
|
||||
|
||||
constructor(id: number, method: "drop" | "send", data?: any) {
|
||||
this.id = id;
|
||||
this.data = { method, data };
|
||||
}
|
||||
}
|
||||
|
||||
window.__msg_queues = window.__msg_queues || [];
|
||||
// In dioxus desktop, eval is copy so we cannot run a drop handler. Instead, the drop handler is run after the channel is garbage collected in the javascript side
|
||||
window.finalizationRegistry =
|
||||
window.finalizationRegistry ||
|
||||
new FinalizationRegistry(({ id }) => {
|
||||
// @ts-ignore - wry gives us this
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({
|
||||
method: "query",
|
||||
params: new QueryParams(id, "drop"),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Get a query from the global state
|
||||
window.getQuery = function (request_id: number): WeakDioxusChannel {
|
||||
return window.__msg_queues[request_id];
|
||||
};
|
||||
|
||||
// Create a new query (and insert it into the global state)
|
||||
window.createQuery = function (request_id: number): NativeDioxusChannel {
|
||||
return new NativeDioxusChannel(request_id);
|
||||
};
|
||||
|
||||
export class NativeDioxusChannel extends DioxusChannel {
|
||||
rust_to_js: Channel;
|
||||
request_id: number;
|
||||
|
||||
constructor(request_id: number) {
|
||||
super();
|
||||
this.rust_to_js = new Channel();
|
||||
this.request_id = request_id;
|
||||
|
||||
window.__msg_queues[request_id] = this.weak();
|
||||
window.finalizationRegistry.register(this, { id: request_id });
|
||||
}
|
||||
|
||||
// Receive message from Rust
|
||||
async recv() {
|
||||
return await this.rust_to_js.recv();
|
||||
}
|
||||
|
||||
// Send message to rust.
|
||||
send(data: any) {
|
||||
// @ts-ignore - wry gives us this
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({
|
||||
method: "query",
|
||||
params: new QueryParams(this.request_id, "send", data),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Send data from rust to javascript
|
||||
rustSend(data: any) {
|
||||
this.rust_to_js.send(data);
|
||||
}
|
||||
|
||||
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
|
||||
async rustRecv(): Promise<any> {}
|
||||
}
|
||||
Reference in new issue
Block a user