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:
Evan Almloff authored and GitHub committed 2024-07-17 20:54:03 -05:00
1 parent a490a94ae5
commit 176e67e5b7
65 files changed
+1703 -495

No files matched your search

+1 -2
View File
@@ -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 = []
+12 -82
View File
@@ -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();
}
-49
View File
@@ -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
View File
@@ -1 +0,0 @@
this files are generated - do not edit them!
-1
View File
@@ -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
View File
@@ -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};
-3
View File
@@ -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 {
-108
View File
@@ -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> {}
}