diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index f7db1c56d1..aa7bb2ea0f 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -33,7 +33,7 @@ jobs: cache-on-failure: "false" - uses: cargo-bins/cargo-binstall@main - name: Install CLI - run: cargo binstall dioxus-cli -y --force --version 0.7.0-rc.0 + run: cargo binstall dioxus-cli -y --force --version 0.7.0-rc.1 - name: Build run: cd packages/docsite && dx build --verbose --trace --platform web --fullstack true --features fullstack,production --release --ssg - name: Generate search index diff --git a/Cargo.lock b/Cargo.lock index d0eb68ec51..fca61d6335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,16 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-parser" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43e7fd8284f025d0bd143c2855618ecdf697db55bde39211e5c9faec7669173" +dependencies = [ + "heapless 0.8.0", + "nom 7.1.3", +] + [[package]] name = "anstream" version = "0.6.21" @@ -147,7 +157,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -222,7 +232,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -244,7 +254,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -255,7 +265,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -326,19 +336,19 @@ checksum = "ebb4bd301db2e2ca1f5be131c24eb8ebf2d9559bc3744419e93baf8ddea7e670" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "av1-grain" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -352,15 +362,42 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa 1.0.15", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core", + "axum-core 0.5.5", "axum-macros", - "base64", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -370,7 +407,7 @@ dependencies = [ "hyper", "hyper-util", "itoa 1.0.15", - "matchit", + "matchit 0.8.4", "memchr", "mime", "multer", @@ -396,11 +433,31 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f08a543641554404b42acd0d2494df12ca2be034d7b8ee4dbbf7446f940a2ef" dependencies = [ - "axum", + "axum 0.8.6", "client-ip", "serde", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.5.5" @@ -426,8 +483,8 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum", - "axum-core", + "axum 0.8.6", + "axum-core 0.5.5", "bytes", "futures-util", "headers", @@ -451,7 +508,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -460,6 +517,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -651,9 +714,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" dependencies = [ "find-msvc-tools", "jobserver", @@ -706,7 +769,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" dependencies = [ - "base64", + "base64 0.22.1", "encoding_rs", ] @@ -782,7 +845,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -898,6 +961,45 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic 0.12.3", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.12.3", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "const-serialize" version = "0.7.0-rc.3" @@ -916,7 +1018,7 @@ checksum = "7c7a0c525c8d315f5195430912463f41dd5e274853b41b8bdc967f2762ad7cf6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1185,7 +1287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1208,7 +1310,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1219,7 +1321,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1244,9 +1346,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1272,7 +1374,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1292,7 +1394,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "unicode-xid", ] @@ -1379,7 +1481,7 @@ dependencies = [ "quote", "regex", "serde", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1440,7 +1542,7 @@ dependencies = [ "dioxus-rsx", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1456,7 +1558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e8e3591e84c53818c51fbb7edbd146f3b4a28d897ab8daa12a069b85bf00b87" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "bytes", "cocoa", "core-foundation 0.10.1", @@ -1477,7 +1579,7 @@ dependencies = [ "global-hotkey", "infer", "jni", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0-rc.3", "libc", "muda", "ndk", @@ -1616,7 +1718,7 @@ dependencies = [ "askama_escape 0.10.3", "async-recursion", "automod", - "axum", + "axum 0.8.6", "chrono", "dioxus", "dioxus-cli-config", @@ -1663,7 +1765,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0-rc.3", "serde", "serde_json", "tracing", @@ -1691,10 +1793,10 @@ dependencies = [ "anyhow", "async-stream", "async-tungstenite", - "axum", - "axum-core", + "axum 0.8.6", + "axum-core 0.5.5", "axum-extra", - "base64", + "base64 0.22.1", "bytes", "ciborium", "const-str", @@ -1721,7 +1823,7 @@ dependencies = [ "inventory", "js-sys", "mime", - "pin-project", + "pin-project 1.1.10", "reqwest", "rustversion", "send_wrapper", @@ -1754,8 +1856,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5e5b2a384abe1c4ba5572bba5495a1566f533e4f0bf281cc731f5a5642831b" dependencies = [ "anyhow", - "axum-core", - "base64", + "axum-core 0.5.5", + "base64 0.22.1", "ciborium", "dioxus-core", "dioxus-document", @@ -1783,7 +1885,7 @@ dependencies = [ "convert_case 0.8.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "xxhash-rust", ] @@ -1834,7 +1936,7 @@ dependencies = [ "futures-util", "generational-box", "keyboard-types", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0-rc.3", "rustversion", "serde", "serde_json", @@ -1851,7 +1953,7 @@ dependencies = [ "convert_case 0.8.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1864,7 +1966,7 @@ dependencies = [ "dioxus-core-types", "dioxus-html", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0-rc.3", "rustc-hash 2.1.1", "serde", "sledgehammer_bindgen", @@ -1880,7 +1982,7 @@ version = "0.7.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d78671230db6e2233a7f00bf74e80fa5bb79e63a2934962bdc1579b013f83e2" dependencies = [ - "axum", + "axum 0.8.6", "dioxus-cli-config", "dioxus-core", "dioxus-devtools", @@ -1918,7 +2020,8 @@ dependencies = [ name = "dioxus-playground" version = "0.1.0" dependencies = [ - "base64", + "ansi-parser", + "base64 0.22.1", "dioxus", "dioxus-autofmt", "dioxus-core", @@ -1926,12 +2029,14 @@ dependencies = [ "dioxus-devtools", "dioxus-document", "dioxus-html", + "dioxus-primitives", "dioxus-rsx", "dioxus-rsx-hotreload", "dioxus-rsx-rosetta", "example-projects", "futures", "gloo-net", + "gloo-timers", "gloo-utils", "miniz_oxide", "model", @@ -1939,10 +2044,25 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", - "syn 2.0.107", + "syn 2.0.108", "thiserror 2.0.17", + "tracing", "uuid", "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "dioxus-primitives" +version = "0.0.1" +source = "git+https://github.com/DioxusLabs/components#0067c31415c26b6215909f573d6c03d3624990d5" +dependencies = [ + "dioxus", + "dioxus-time", + "lazy-js-bundle 0.6.2", + "num-integer", + "time", + "tracing", ] [[package]] @@ -1978,7 +2098,7 @@ dependencies = [ "quote", "sha2", "slab", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -1990,7 +2110,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2006,7 +2126,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.107", + "syn 2.0.108", "tracing", ] @@ -2024,7 +2144,7 @@ dependencies = [ "htmlentity", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2050,7 +2170,7 @@ name = "dioxus-search-macro" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2078,8 +2198,8 @@ checksum = "60720b456f09e4653c53fa1103f438a3a2109188727bbc271e6710b84a84dfc6" dependencies = [ "anyhow", "async-trait", - "axum", - "base64", + "axum 0.8.6", + "base64 0.22.1", "bytes", "chrono", "ciborium", @@ -2110,7 +2230,7 @@ dependencies = [ "inventory", "lru", "parking_lot", - "pin-project", + "pin-project 1.1.10", "rustc-hash 2.1.1", "serde", "serde_json", @@ -2176,7 +2296,18 @@ dependencies = [ "convert_case 0.8.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-time" +version = "0.7.0-rc.3" +source = "git+https://github.com/ealmloff/dioxus-std?branch=0.7#4b21134c3950d219a6e42ba33b6ba4bda97c062b" +dependencies = [ + "dioxus", + "futures", + "gloo-timers", + "tokio", ] [[package]] @@ -2200,7 +2331,7 @@ dependencies = [ "generational-box", "gloo-timers", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0-rc.3", "rustc-hash 2.1.1", "send_wrapper", "serde", @@ -2220,7 +2351,7 @@ dependencies = [ "askama_escape 0.10.3", "async-recursion", "automod", - "axum", + "axum 0.8.6", "chrono", "dioxus", "dioxus-docs-03", @@ -2230,6 +2361,7 @@ dependencies = [ "dioxus-docs-07", "dioxus-docs-blog", "dioxus-docs-examples", + "dioxus-playground", "dioxus-search", "dioxus-web", "futures", @@ -2302,7 +2434,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2334,20 +2466,20 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] name = "doc-comment" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -2448,7 +2580,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2469,7 +2601,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2489,7 +2621,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2606,7 +2738,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2636,9 +2768,9 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -2689,7 +2821,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2713,6 +2845,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + [[package]] name = "frontmatter" version = "0.4.0" @@ -2807,7 +2949,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -2822,6 +2964,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -3087,7 +3235,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -3129,7 +3277,7 @@ dependencies = [ "gloo-utils", "http", "js-sys", - "pin-project", + "pin-project 1.1.10", "serde", "serde_json", "thiserror 1.0.69", @@ -3189,6 +3337,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.15.5", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "gtk" version = "0.18.2" @@ -3238,7 +3409,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -3253,7 +3424,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -3280,6 +3451,21 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -3320,13 +3506,26 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom 7.1.3", + "num-traits", +] + [[package]] name = "headers" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "headers-core", "http", @@ -3351,13 +3550,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32", + "hash32 0.2.1", "rustc_version", "serde", "spin", "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -3480,6 +3689,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "1.7.0" @@ -3520,6 +3735,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -3542,7 +3770,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -3554,7 +3782,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -3758,6 +3986,16 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -3770,9 +4008,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e0ddd45fe8e09ee1a607920b12271f8a5528a41ecaf6e1d1440d6493315b6b" +checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" dependencies = [ "console", "portable-atomic", @@ -3807,7 +4045,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -3850,6 +4088,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -3919,9 +4166,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -3967,10 +4214,16 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser 0.29.6", "html5ever 0.29.1", - "indexmap", + "indexmap 2.12.0", "selectors 0.24.0", ] +[[package]] +name = "lazy-js-bundle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" + [[package]] name = "lazy-js-bundle" version = "0.7.0-rc.3" @@ -4108,9 +4361,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -4171,7 +4424,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4239,7 +4492,7 @@ dependencies = [ "manganis-core", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4278,7 +4531,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4296,6 +4549,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -4343,7 +4602,7 @@ dependencies = [ "serde", "serde_json", "sublime-color-scheme", - "syn 2.0.107", + "syn 2.0.108", "syntect", ] @@ -4357,7 +4616,7 @@ dependencies = [ "mdbook-shared", "prettyplease", "serde", - "syn 2.0.107", + "syn 2.0.108", "use-mdbook", ] @@ -4377,7 +4636,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.107", + "syn 2.0.108", "syntect", ] @@ -4477,7 +4736,8 @@ dependencies = [ name = "model" version = "0.1.0" dependencies = [ - "axum", + "axum 0.8.6", + "dioxus-devtools", "dioxus-document", "dioxus-dx-wire-format", "dioxus-logger", @@ -4492,9 +4752,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -4620,6 +4880,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -4639,6 +4920,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -4663,7 +4953,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4724,7 +5014,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4899,7 +5189,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -4957,7 +5247,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5056,7 +5346,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5189,7 +5479,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5219,13 +5509,33 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + [[package]] name = "pin-project" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ - "pin-project-internal", + "pin-project-internal 1.1.10", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -5236,7 +5546,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5263,8 +5573,8 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ - "base64", - "indexmap", + "base64 0.22.1", + "indexmap 2.12.0", "quick-xml 0.38.3", "serde", "time", @@ -5317,7 +5627,7 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", - "heapless", + "heapless 0.7.17", "serde", ] @@ -5368,7 +5678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -5431,9 +5741,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -5446,7 +5756,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "version_check", ] @@ -5466,7 +5776,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", ] [[package]] @@ -5543,6 +5885,21 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -5580,7 +5937,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -5617,7 +5974,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] @@ -5761,7 +6118,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -5797,6 +6154,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -5884,7 +6250,7 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "cookie", "cookie_store", @@ -6064,9 +6430,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "once_cell", "ring", @@ -6078,9 +6444,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -6291,7 +6657,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6337,7 +6703,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6365,16 +6731,22 @@ dependencies = [ name = "server" version = "0.1.0" dependencies = [ - "axum", + "axum 0.8.6", "axum-client-ip", + "console-subscriber", + "dashmap", "dioxus", + "dioxus-devtools-types", "dioxus-dx-wire-format", "dioxus-logger", + "dioxus-primitives", "example-projects", "fs_extra", "futures", + "governor", "model", "reqwest", + "rustix", "serde", "serde_json", "thiserror 2.0.17", @@ -6382,6 +6754,10 @@ dependencies = [ "tokio-util", "tower 0.4.13", "tower-http 0.5.2", + "tower-util", + "tower_governor", + "tracing", + "tracing-subscriber", "uuid", ] @@ -6520,7 +6896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" dependencies = [ "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6559,6 +6935,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -6604,6 +6990,15 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "srtparse" version = "0.2.0" @@ -6739,9 +7134,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.107" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -6765,7 +7160,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6795,7 +7190,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "syntect", ] @@ -6882,7 +7277,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6947,7 +7342,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -6958,7 +7353,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7052,7 +7447,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -7066,7 +7461,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7197,7 +7592,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -7210,7 +7605,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap", + "indexmap 2.12.0", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -7221,7 +7616,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -7235,7 +7630,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap", + "indexmap 2.12.0", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -7256,6 +7651,65 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project 1.1.10", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum 0.8.6", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project 1.1.10", + "socket2 0.6.1", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -7263,7 +7717,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project 1.1.10", "pin-project-lite", + "rand 0.8.5", + "slab", "tokio", "tokio-util", "tower-layer", @@ -7279,9 +7738,12 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -7354,6 +7816,35 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1093c19826d33807c72511e68f73b4a0469a3f22c2bd5f7d5212178b4b89674" +dependencies = [ + "futures-core", + "futures-util", + "pin-project 0.4.30", + "tower-service", +] + +[[package]] +name = "tower_governor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" +dependencies = [ + "axum 0.8.6", + "forwarded-header-value", + "governor", + "http", + "pin-project 1.1.10", + "thiserror 2.0.17", + "tonic 0.14.2", + "tower 0.5.2", + "tracing", +] + [[package]] name = "tracing" version = "0.1.41" @@ -7374,7 +7865,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7384,6 +7875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -7392,10 +7884,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ - "pin-project", + "pin-project 1.1.10", "tracing", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" @@ -7403,12 +7906,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", + "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -7627,6 +8133,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7670,7 +8182,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" dependencies = [ - "pin-project", + "pin-project 1.1.10", "tracing", "warnings-macro", ] @@ -7683,7 +8195,7 @@ checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -7709,9 +8221,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -7722,25 +8234,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.107", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -7751,9 +8249,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7761,22 +8259,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.107", - "wasm-bindgen-backend", + "syn 2.0.108", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -7856,9 +8354,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -7965,7 +8463,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8083,7 +8581,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8094,7 +8592,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8451,7 +8949,7 @@ version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" dependencies = [ - "base64", + "base64 0.22.1", "block2", "cookie", "crossbeam-channel", @@ -8578,7 +9076,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "synstructure", ] @@ -8620,7 +9118,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "zbus_names", "zvariant", "zvariant_utils", @@ -8655,7 +9153,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8675,7 +9173,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "synstructure", ] @@ -8715,7 +9213,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", ] [[package]] @@ -8766,7 +9264,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.107", + "syn 2.0.108", "zvariant_utils", ] @@ -8779,6 +9277,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.107", + "syn 2.0.108", "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index 25056eaa79..cb7f74d3c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,10 +80,11 @@ dioxus-html = { version = "0.7.0-rc.3", default-features = false } dioxus-rsx-rosetta = "0.7.0-rc.3" dioxus-autofmt = "0.7.0-rc.3" dioxus-dx-wire-format = "0.7.0-rc.3" +dioxus-devtools-types = "0.7.0-rc.3" dioxus-logger = "0.7.0-rc.3" -# 3rd-party dioxus -# dioxus-sdk = { version = "0.6", default-features = false } +# # 3rd-party dioxus +# dioxus-sdk = { git = "https://github.com/ealmloff/dioxus-std", branch = "0.7" } getrandom = { version = "0.2" } serde = { version = "1.0.215", features = ["derive"] } @@ -120,59 +121,3 @@ opt-level = 3 codegen-units = 1 -[patch.crates-io] -# dioxus = { path = "../dioxus/packages/dioxus" } -# dioxus-lib = { path = "../dioxus/packages/dioxus-lib" } -# dioxus-core = { path = "../dioxus/packages/core" } -# dioxus-core-macro = { path = "../dioxus/packages/core-macro" } -# dioxus-config-macro = { path = "../dioxus/packages/config-macro" } -# dioxus-router = { path = "../dioxus/packages/router" } -# dioxus-router-macro = { path = "../dioxus/packages/router-macro" } -# dioxus-html = { path = "../dioxus/packages/html" } -# dioxus-html-internal-macro = { path = "../dioxus/packages/html-internal-macro" } -# dioxus-hooks = { path = "../dioxus/packages/hooks" } -# dioxus-web = { path = "../dioxus/packages/web" } -# dioxus-ssr = { path = "../dioxus/packages/ssr" } -# dioxus-desktop = { path = "../dioxus/packages/desktop" } -# dioxus-interpreter-js = { path = "../dioxus/packages/interpreter" } -# dioxus-liveview = { path = "../dioxus/packages/liveview" } -# dioxus-rsx = { path = "../dioxus/packages/rsx" } -# dioxus-signals = { path = "../dioxus/packages/signals" } -# dioxus-cli-config = { path = "../dioxus/packages/cli-config" } -# generational-box = { path = "../dioxus/packages/generational-box" } -# dioxus_server_macro = { path = "../dioxus/packages/server-macro" } -# dioxus-fullstack = { path = "../dioxus/packages/fullstack" } -# dioxus-autofmt = { path = "../dioxus/packages/autofmt" } -# dioxus-devtools = { path = "../dioxus/packages/devtools" } -# dioxus-devtools-types = { path = "../dioxus/packages/devtools-types" } -# manganis = { path = "../dioxus/packages/manganis/manganis" } -# manganis-core = { path = "../dioxus/packages/manganis/manganis-core" } -# manganis-macro = { path = "../dioxus/packages/manganis/manganis-macro" } - -# dioxus = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-lib = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-core = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-core-macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-config-macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-router = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-router-macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-html = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-html-internal-macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-hooks = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-web = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-ssr = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-desktop = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-interpreter-js = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-liveview = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-rsx = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-signals = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-cli-config = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# generational-box = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus_server_macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-fullstack = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-autofmt = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-devtools = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# dioxus-devtools-types = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# manganis = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# manganis-core = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } -# manganis-macro = { git = "https://github.com/dioxuslabs/dioxus", rev ="e00ebec8048d8ca934fff918d2d1432bf6ce7640" } diff --git a/Dockerfile b/Dockerfile index f3bb6ed5d4..205fc4760c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1-bookworm AS chef +FROM rust:1-trixie AS chef RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash RUN cargo binstall cargo-chef --no-confirm @@ -10,7 +10,7 @@ WORKDIR /app FROM chef AS planner COPY . . -RUN cargo binstall dioxus-cli --root /.cargo --no-confirm +RUN cargo install dioxus-cli --git https://github.com/DioxusLabs/dioxus --root /.cargo RUN cargo chef prepare --recipe-path recipe.json --bin server # Builder @@ -22,7 +22,7 @@ COPY . . RUN cargo build --release --bin server # Pre-slim runtime -FROM rust:1-slim-bookworm AS pre-runtime +FROM rust:1-slim-trixie AS pre-runtime # Install openssl RUN set -ex; \ diff --git a/README.md b/README.md index 10467c5951..5f817cc9be 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ working `Rust` setup: ```sh -cargo binstall dioxus-cli@0.7.0-rc.0 --force +cargo binstall dioxus-cli@0.7.0-rc.1 --force --version 0.7.0-rc.1 ``` With [`dx`][dx] installed, you can use it to build and serve the documentation diff --git a/docs-src/0.6/src/essentials/async/index.md b/docs-src/0.6/src/essentials/async/index.md index 6bf9c23707..aaeae8246c 100644 --- a/docs-src/0.6/src/essentials/async/index.md +++ b/docs-src/0.6/src/essentials/async/index.md @@ -106,12 +106,6 @@ If you need to change the loading view while a specific task is loading, you can {{#include ../docs-router/src/doc_examples/untested_06/asynchronous.rs:suspense_boundary_with_loading_placeholder}} ``` -```inject-dioxus -DemoFrame { - asynchronous::DogGridViewWithLoadingPlaceholder {} -} -``` - ## Suspense with Fullstack To use suspense in your fullstack application, you need to use the `use_server_future` hook instead of `use_resource`. `use_server_future` handles serialization of the result of the future for hydration. It will also suspend automatically, so you don't need to call `.suspend()` on the future. diff --git a/docs-src/0.7/src/essentials/advanced/suspense.md b/docs-src/0.7/src/essentials/advanced/suspense.md index 022e2ba518..75e91c790d 100644 --- a/docs-src/0.7/src/essentials/advanced/suspense.md +++ b/docs-src/0.7/src/essentials/advanced/suspense.md @@ -16,20 +16,6 @@ DemoFrame { } ``` -## Customizing the loading view from children - -If you need to change the loading view while a specific task is loading, you can provide a different loading view with the `with_loading_placeholder` method. The loading placeholder you return from the method will be passed to the suspense boundary and may choose to render it instead of the default loading view: - -```rust -{{#include ../docs-router/src/doc_examples/asynchronous.rs:suspense_boundary_with_loading_placeholder}} -``` - -```inject-dioxus -DemoFrame { - asynchronous::DogGridViewWithLoadingPlaceholder {} -} -``` - ## Suspense with Fullstack Dioxus fullstack will wait for suspended futures during server-side rendering. This means your async data loading starts sooner and search engines can see the resolved version of your page. However, using suspense in fullstack does require some changes for hydration compatibility. diff --git a/fly.toml b/fly.toml index f4daca6e42..85b34b9a8c 100644 --- a/fly.toml +++ b/fly.toml @@ -1,12 +1,12 @@ -# fly.toml app configuration file generated for docsite-playground on 2025-02-04T15:44:00-08:00 +# fly.toml app configuration file generated for docsite-playground-red-wildflower-209-red-wildflower-209 on 2025-10-20T16:41:20-05:00 # # See https://fly.io/docs/reference/configuration/ for information about how to use this file. # -app = 'docsite-playground' -primary_region = 'lax' +app = 'docsite-playground-red-wildflower-209' +primary_region = 'sjc' kill_signal = 'SIGINT' -kill_timeout = '5s' +kill_timeout = '5m0s' [build] @@ -38,6 +38,4 @@ kill_timeout = '5s' soft_limit = 20 [[vm]] - memory = '1gb' - cpu_kind = 'shared' - cpus = 2 + size = 'performance-2x' diff --git a/packages/docs-router/src/doc_examples/__interactive_04.rs b/packages/docs-router/src/doc_examples/__interactive_04.rs index 8d986e2583..af25a3cd07 100644 --- a/packages/docs-router/src/doc_examples/__interactive_04.rs +++ b/packages/docs-router/src/doc_examples/__interactive_04.rs @@ -25,7 +25,7 @@ pub fn component_borrowed_props() -> Element { } pub fn hooks_use_ref() -> Element { - let mut list = use_signal(|| Vec::new()); + let mut list = use_signal(Vec::new); rsx! { p { "Current list: {list:?}" } diff --git a/packages/docs-router/src/doc_examples/asynchronous.rs b/packages/docs-router/src/doc_examples/asynchronous.rs index ab5c57730c..90f82e00fc 100644 --- a/packages/docs-router/src/doc_examples/asynchronous.rs +++ b/packages/docs-router/src/doc_examples/asynchronous.rs @@ -191,7 +191,7 @@ pub fn UseResource() -> Element { pub fn NotCancelSafe() -> Element { // ANCHOR: not_cancel_safe - static RESOURCES_RUNNING: GlobalSignal> = Signal::global(|| HashSet::new()); + static RESOURCES_RUNNING: GlobalSignal> = Signal::global(HashSet::new); let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { // Modify some global state @@ -258,7 +258,7 @@ pub fn NotCancelSafe() -> Element { pub fn CancelSafe() -> Element { // ANCHOR: cancel_safe - static RESOURCES_RUNNING: GlobalSignal> = Signal::global(|| HashSet::new()); + static RESOURCES_RUNNING: GlobalSignal> = Signal::global(HashSet::new); let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { // Modify some global state @@ -613,7 +613,7 @@ mod use_server_future { // ANCHOR: use_server_future #[component] - fn BreedGallery(breed: ReadOnlySignal) -> Element { + fn BreedGallery(breed: ReadSignal) -> Element { // use_server_future is very similar to use_resource, but the value returned from the future // must implement Serialize and Deserialize and it is automatically suspended let response = use_server_future(move || async move { diff --git a/packages/docs-router/src/doc_examples/component_lifecycle.rs b/packages/docs-router/src/doc_examples/component_lifecycle.rs index 33966cd7d6..09e8071f86 100644 --- a/packages/docs-router/src/doc_examples/component_lifecycle.rs +++ b/packages/docs-router/src/doc_examples/component_lifecycle.rs @@ -101,9 +101,7 @@ mod effect { // You can use them to read or modify the rendered component use_effect(|| { log!("Effect ran"); - document::eval(&format!( - "document.getElementById('effect-output').innerText = 'Effect ran'" - )); + document::eval("document.getElementById('effect-output').innerText = 'Effect ran'"); }); rsx! { diff --git a/packages/docs-router/src/doc_examples/data_fetching.rs b/packages/docs-router/src/doc_examples/data_fetching.rs index 9ea94d57ad..b67b4e8b79 100644 --- a/packages/docs-router/src/doc_examples/data_fetching.rs +++ b/packages/docs-router/src/doc_examples/data_fetching.rs @@ -9,23 +9,20 @@ mod waterfall_effect { } // ANCHOR: waterfall_effect - fn fetch_dog_image( - breed: impl Display, - ) -> impl Future> { - async move { - let response = reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) - .await? - .json::() - .await?; - Ok(response.message) - } + async fn fetch_dog_image(breed: impl Display) -> dioxus::Result { + let response = reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) + .await? + .json::() + .await?; + dioxus::Ok(response.message) } #[component] fn DogView() -> Element { let poodle_img = use_resource(|| fetch_dog_image("poodle")); - let poodle_img = match poodle_img() { + let read = poodle_img.read(); + let poodle_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { @@ -36,7 +33,8 @@ mod waterfall_effect { let golden_retriever_img = use_resource(|| fetch_dog_image("golden retriever")); - let golden_retriever_img = match golden_retriever_img() { + let read = golden_retriever_img.read(); + let golden_retriever_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { @@ -47,7 +45,8 @@ mod waterfall_effect { let pug_img = use_resource(|| fetch_dog_image("pug")); - let pug_img = match pug_img() { + let read = pug_img.read(); + let pug_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { @@ -78,14 +77,12 @@ mod no_waterfall_effect { message: String, } - fn fetch_dog_image(breed: impl Display) -> impl Future> { - async move { - let response = reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) - .await? - .json::() - .await?; - Ok(response.message) - } + async fn fetch_dog_image(breed: impl Display) -> dioxus::Result { + let response = reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) + .await? + .json::() + .await?; + dioxus::Ok(response.message) } #[component] @@ -95,7 +92,8 @@ mod no_waterfall_effect { let golden_retriever_img = use_resource(|| fetch_dog_image("golden retriever")); let pug_img = use_resource(|| fetch_dog_image("pug")); - let poodle_img = match poodle_img() { + let read = poodle_img.read(); + let poodle_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { @@ -103,7 +101,8 @@ mod no_waterfall_effect { }; } }; - let golden_retriever_img = match golden_retriever_img() { + let read = golden_retriever_img.read(); + let golden_retriever_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { @@ -111,7 +110,8 @@ mod no_waterfall_effect { }; } }; - let pug_img = match pug_img() { + let read = pug_img.read(); + let pug_img = match read.as_ref() { Some(Ok(src)) => src, _ => { return rsx! { diff --git a/packages/docs-router/src/doc_examples/error_handling.rs b/packages/docs-router/src/doc_examples/error_handling.rs index 6a990854d5..2a915a4017 100644 --- a/packages/docs-router/src/doc_examples/error_handling.rs +++ b/packages/docs-router/src/doc_examples/error_handling.rs @@ -171,7 +171,7 @@ mod phone_number_validation { // ANCHOR: phone_number_validation #[component] pub fn PhoneNumberValidation() -> Element { - let mut phone_number = use_signal(|| String::new()); + let mut phone_number = use_signal(String::new); let parsed_phone_number = use_memo(move || phone_number().parse::()); rsx! { diff --git a/packages/docs-router/src/doc_examples/hackernews_async.rs b/packages/docs-router/src/doc_examples/hackernews_async.rs index 896ec12067..fd9009aedd 100644 --- a/packages/docs-router/src/doc_examples/hackernews_async.rs +++ b/packages/docs-router/src/doc_examples/hackernews_async.rs @@ -129,7 +129,7 @@ pub mod fetch { } #[component] - fn StoryListing(story: ReadOnlySignal) -> Element { + fn StoryListing(story: ReadSignal) -> Element { let mut preview_state = consume_context::>(); let StoryItem { title, @@ -297,7 +297,7 @@ async fn resolve_story( } #[component] -fn StoryListing(story: ReadOnlySignal) -> Element { +fn StoryListing(story: ReadSignal) -> Element { let mut preview_state = consume_context::>(); let StoryItem { title, diff --git a/packages/docs-router/src/doc_examples/hackernews_complete.rs b/packages/docs-router/src/doc_examples/hackernews_complete.rs index 7e4bd7d69d..a1be7153d7 100644 --- a/packages/docs-router/src/doc_examples/hackernews_complete.rs +++ b/packages/docs-router/src/doc_examples/hackernews_complete.rs @@ -50,7 +50,7 @@ async fn resolve_story( } #[component] -fn StoryListing(story: ReadOnlySignal) -> Element { +fn StoryListing(story: ReadSignal) -> Element { let preview_state = consume_context::>(); let StoryItem { title, diff --git a/packages/docs-router/src/doc_examples/hackernews_post.rs b/packages/docs-router/src/doc_examples/hackernews_post.rs index 08964291f7..b79ab2ff5f 100644 --- a/packages/docs-router/src/doc_examples/hackernews_post.rs +++ b/packages/docs-router/src/doc_examples/hackernews_post.rs @@ -170,7 +170,7 @@ pub mod story_v6 { } #[component] - fn StoryListing(story: ReadOnlySignal) -> Element { + fn StoryListing(story: ReadSignal) -> Element { let StoryItem { title, url, @@ -268,7 +268,7 @@ pub mod story_final { } #[component] - fn StoryListing(story: ReadOnlySignal) -> Element { + fn StoryListing(story: ReadSignal) -> Element { let StoryItem { title, url, diff --git a/packages/docs-router/src/doc_examples/hackernews_state.rs b/packages/docs-router/src/doc_examples/hackernews_state.rs index b5725c45ed..1d1fa7ae10 100644 --- a/packages/docs-router/src/doc_examples/hackernews_state.rs +++ b/packages/docs-router/src/doc_examples/hackernews_state.rs @@ -79,7 +79,7 @@ pub mod app_v1 { // ANCHOR_END: app_v1 #[component] - fn StoryListing(story: ReadOnlySignal) -> Element { + fn StoryListing(story: ReadSignal) -> Element { let StoryItem { title, url, @@ -190,7 +190,7 @@ mod story_listing_listener { } #[component] - fn StoryListing(story: ReadOnlySignal) -> Element { + fn StoryListing(story: ReadSignal) -> Element { let mut preview_state = consume_context::>(); let StoryItem { title, @@ -260,7 +260,7 @@ pub fn App() -> Element { // ANCHOR: shared_state_stories #[component] -fn StoryListing(story: ReadOnlySignal) -> Element { +fn StoryListing(story: ReadSignal) -> Element { let mut preview_state = consume_context::>(); let StoryItem { title, diff --git a/packages/docs-router/src/doc_examples/reactivity.rs b/packages/docs-router/src/doc_examples/reactivity.rs index 0b11f1a179..49038ae37a 100644 --- a/packages/docs-router/src/doc_examples/reactivity.rs +++ b/packages/docs-router/src/doc_examples/reactivity.rs @@ -348,11 +348,11 @@ mod non_reactive_state { use super::*; // ANCHOR: making_props_reactive - // You can track props by wrapping the type in a ReadOnlySignal - // Dioxus will automatically convert T into ReadOnlySignal when you pass + // You can track props by wrapping the type in a ReadSignal + // Dioxus will automatically convert T into ReadSignal when you pass // props to the component #[component] - fn Count(count: ReadOnlySignal) -> Element { + fn Count(count: ReadSignal) -> Element { // Then when you read count inside the memo, it subscribes to the count signal let double_count = use_memo(move || count() * 2); diff --git a/packages/docs-router/src/doc_examples/use_effect.rs b/packages/docs-router/src/doc_examples/use_effect.rs index 9fd6f19802..8cfb640d98 100644 --- a/packages/docs-router/src/doc_examples/use_effect.rs +++ b/packages/docs-router/src/doc_examples/use_effect.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; #[component] -fn Profile(id: ReadOnlySignal) -> Element { +fn Profile(id: ReadSignal) -> Element { // Only change the page title when the id changes use_effect(move || { // We read the id signal here, so it will automatically be added as a dependency for the effect diff --git a/packages/docs-router/src/doc_examples/use_resource.rs b/packages/docs-router/src/doc_examples/use_resource.rs index dcb4a16933..43b1600769 100644 --- a/packages/docs-router/src/doc_examples/use_resource.rs +++ b/packages/docs-router/src/doc_examples/use_resource.rs @@ -42,7 +42,7 @@ pub fn App() -> Element { } #[component] -fn RandomDog(breed: ReadOnlySignal) -> Element { +fn RandomDog(breed: ReadSignal) -> Element { // ANCHOR: dependency let future = use_resource(move || async move { reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) diff --git a/packages/docsite/Cargo.toml b/packages/docsite/Cargo.toml index cbcb9037ac..ae52432493 100644 --- a/packages/docsite/Cargo.toml +++ b/packages/docsite/Cargo.toml @@ -24,7 +24,7 @@ syntect-html = { workspace = true } mdbook-shared = { workspace = true } use-mdbook = { workspace = true } dioxus-search = { workspace = true } -# dioxus-playground = { workspace = true } +dioxus-playground = { workspace = true } askama_escape = { version = "0.10.3", optional = true } getrandom = { workspace = true, features = ["js"] } diff --git a/packages/docsite/assets/main.css b/packages/docsite/assets/main.css index 047b1fc7c7..3a72f0dddd 100644 --- a/packages/docsite/assets/main.css +++ b/packages/docsite/assets/main.css @@ -1,88 +1,88 @@ @media (min-width: 767px) { - .styled-scrollbar { - scrollbar-width: thin; - scrollbar-color: #21252900 transparent; - scrollbar-gutter: stable; - overflow: auto; - } - - .styled-scrollbar::-webkit-scrollbar { - height: 0.5rem; - width: 0.375rem; - } - - .styled-scrollbar::-webkit-scrollbar-track { - background-color: transparent; - } - - .styled-scrollbar::-webkit-scrollbar-thumb { - border-radius: 0.375rem; - border: 3px solid transparent; - background-clip: content-box; - scrollbar-width: thin; - scrollbar-color: #0080ff #fff; - } - - /* safari bug, the thumb doesn't change unless we trigger a hover event on the item itself */ - .styled-scrollbar:hover { - min-height: 1px; - scrollbar-color: #212529 transparent; - } - - .styled-scrollbar:hover::-webkit-scrollbar-thumb { - border-radius: 0.375rem; - border: 3px solid transparent; - background-clip: content-box; - scrollbar-width: thin; - scrollbar-color: #0080ff #fff; - background: #d0d3d7; - } + .styled-scrollbar { + scrollbar-width: thin; + scrollbar-color: #21252900 transparent; + scrollbar-gutter: stable; + overflow: auto; + } + + .styled-scrollbar::-webkit-scrollbar { + height: 0.5rem; + width: 0.375rem; + } + + .styled-scrollbar::-webkit-scrollbar-track { + background-color: transparent; + } + + .styled-scrollbar::-webkit-scrollbar-thumb { + border-radius: 0.375rem; + border: 3px solid transparent; + background-clip: content-box; + scrollbar-width: thin; + scrollbar-color: #0080ff #fff; + } + + /* safari bug, the thumb doesn't change unless we trigger a hover event on the item itself */ + .styled-scrollbar:hover { + min-height: 1px; + scrollbar-color: #212529 transparent; + } + + .styled-scrollbar:hover::-webkit-scrollbar-thumb { + border-radius: 0.375rem; + border: 3px solid transparent; + background-clip: content-box; + scrollbar-width: thin; + scrollbar-color: #0080ff #fff; + background: #d0d3d7; + } } .playground-container { - height: 900px; - display: flex; - flex-direction: column; - /* width: 100vw; - height: 80vh; - display: flex; - flex-direction: column; */ + width: 100vw; + height: 80vh; + display: flex; + flex-direction: column; } html { - &:where([data-theme="dark"], [data-theme="dark"] *) { - background-color: black; - } + &:where([data-theme="dark"], [data-theme="dark"] *) { + background-color: black; + } } .markdown-body > div + p { - margin-top: 2rem; + margin-top: 2rem; } -.markdown-body > video, .markdown-body > img { - margin-bottom: 2rem; +.markdown-body > video, +.markdown-body > img { + margin-bottom: 2rem; } - .codeblock { - font-weight: 400; + font-weight: 400; } .codeblock > pre { + /* &:where([data-theme="light"], [data-theme="light"] *) { + background-color: rgb(37, 36, 36) !important; + } */ /* &:where([data-theme="light"], [data-theme="light"] *) { background-color: rgb(37, 36, 36) !important; } */ } .codeblock > pre { - border-radius: 0px 0px 0px 0px; - margin-bottom: 0px !important; + border-radius: 0px 0px 0px 0px; + margin-bottom: 0px !important; } .markdown-body { - box-sizing: border-box; - min-width: 200px; - list-style: disc; + box-sizing: border-box; + min-width: 200px; + list-style: disc; } /* @@ -90,18 +90,18 @@ https: //stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjus This way clicking on headers snaps to the height of the navbar + some padding */ :target { - scroll-margin-top: calc(4rem + 8px); + scroll-margin-top: calc(4rem + 8px); } @media (max-width: 767px) { - .markdown-body { - /* padding: 15px; */ - } + .markdown-body { + /* padding: 15px; */ + } } .main-side-nav { - max-height: calc(100vh - 4rem); - overflow: auto; + max-height: calc(100vh - 4rem); + overflow: auto; } /* on small screens we want to hide the copy div @@ -109,139 +109,138 @@ we have to select it based on the content since the styling is buried deep in md It's so unliklely anyone is copying text on mobile that we can just hide it */ @media (max-width: 767px) { - .markdown-body - button[onclick="navigator.clipboard.writeText(this.previousElementSibling.innerText)"] { - display: none; - } + .markdown-body + button[onclick="navigator.clipboard.writeText(this.previousElementSibling.innerText)"] { + display: none; + } } .dioxus-demo input { - border: 1px solid #ced4da; - border-radius: 5px; - background-color: white; - padding: 5px; - margin: 5px; - max-width: 150px; + border: 1px solid #ced4da; + border-radius: 5px; + background-color: white; + padding: 5px; + margin: 5px; + max-width: 150px; } .dioxus-demo { - border-width: 1px; - border-color: #ced4da; + border-width: 1px; + border-color: #ced4da; - /* text-align: center; */ + /* text-align: center; */ } .dioxus-demo h1 { - margin-top: 16px; + margin-top: 16px; } .dioxus-show { - z-index: 10000; - visibility: visible; - transition: opacity 0.1s, scale 0.1s; - opacity: 1; - scale: 1; + z-index: 10000; + visibility: visible; + transition: + opacity 0.1s, + scale 0.1s; + opacity: 1; + scale: 1; } .dioxus-hide { - z-index: -1; - visibility: hidden; - opacity: 0; - scale: 1.1; + z-index: -1; + visibility: hidden; + opacity: 0; + scale: 1.1; } .markdown-body ul { - list-style: disc; + list-style: disc; } .markdown-body img { - max-height: 600px; + max-height: 600px; } .markdown-body video { - max-height: 600px; + max-height: 600px; } .markdown-body li { - display: list-item; + display: list-item; } .markdown-body ol { - list-style: decimal; + list-style: decimal; } .dioxus-blog-post img, .centered-overflow { - max-height: 700px; - max-width: 100%; - /* max-width: min(1200px, 95vw); */ - width: auto; - margin-left: 50%; - transform: translateX(-50%); - margin-bottom: 1rem; - border-radius: 6px; + max-height: 700px; + max-width: 100%; + /* max-width: min(1200px, 95vw); */ + width: auto; + margin-left: 50%; + transform: translateX(-50%); + margin-bottom: 1rem; + border-radius: 6px; } .dioxus-blog-post h2 { - margin-top: 2.5rem; - padding-top: 2.5rem; - padding-bottom: 0.25em; + margin-top: 2.5rem; + padding-top: 2.5rem; + padding-bottom: 0.25em; } .highlight pre, .markdown-body pre { - background-color: #1e1e1e; + background-color: #1e1e1e; } -.dioxus-blog-post h1 { - /* text-align: center; */ -} .dioxus-blog-post - :where(h2:not(:is(h1 + h2))):not( - :where([class~="not-prose"], [class~="not-prose"] *) - ) { - border-top-style: solid; - border-top-width: 1px; - border-color: #e5e7eb; + :where(h2:not(:is(h1 + h2))):not( + :where([class~="not-prose"], [class~="not-prose"] *) + ) { + border-top-style: solid; + border-top-width: 1px; + border-color: #e5e7eb; } .navbar_externalArrow___VWBd { - position: absolute; - top: 4px; - right: 0px; + position: absolute; + top: 4px; + right: 0px; } .markdown-body ul { - list-style: disc; + list-style: disc; } .markdown-body ol { - list-style: decimal; + list-style: decimal; } .markdown-body li { - display: list-item; + display: list-item; } .markdown-body > div > button { - display: inline-block; - background-color: rgba(209, 213, 219, 0.3); - border-radius: 0.25rem; - padding: 0.25rem 0.5rem; - border: 1px solid #ced4da; - margin: 0.25rem; + display: inline-block; + background-color: rgba(209, 213, 219, 0.3); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + border: 1px solid #ced4da; + margin: 0.25rem; } .markdown-body .header { - color: inherit; + color: inherit; } .textured-body { - background-repeat: repeat; - background-image: url("data:image/png;base64,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"); + background-repeat: repeat; + background-image: url("data:image/png;base64,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"); } @media (prefers-color-scheme: dark) { - .textured-body { - color: #e2e5e9; - background-repeat: repeat; - background-image: url("data:image/png;base64,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"); - } + .textured-body { + color: #e2e5e9; + background-repeat: repeat; + background-image: url("data:image/png;base64,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"); + } } diff --git a/packages/docsite/src/components.rs b/packages/docsite/src/components.rs index b35e68dfd2..10610b122e 100644 --- a/packages/docsite/src/components.rs +++ b/packages/docsite/src/components.rs @@ -14,8 +14,8 @@ pub mod nav; pub use nav::*; pub mod notfound; pub use notfound::*; -// pub mod playground; -// pub use playground::*; +pub mod playground; +pub use playground::*; pub mod search; pub use search::*; pub mod component_demo; diff --git a/packages/docsite/src/components/awesome.rs b/packages/docsite/src/components/awesome.rs index 15a95cf47b..1b802df4a5 100644 --- a/packages/docsite/src/components/awesome.rs +++ b/packages/docsite/src/components/awesome.rs @@ -193,7 +193,7 @@ pub(crate) fn Awesome() -> Element { } #[component] -fn AwesomeItem(item: ReadOnlySignal) -> Element { +fn AwesomeItem(item: ReadSignal) -> Element { let stars = use_resource(move || async move { let item = item.read(); let is_github = item.github.is_some(); diff --git a/packages/docsite/src/components/component_demo.rs b/packages/docsite/src/components/component_demo.rs index d48da0aab5..37c4d229c7 100644 --- a/packages/docsite/src/components/component_demo.rs +++ b/packages/docsite/src/components/component_demo.rs @@ -3,8 +3,8 @@ use dioxus::prelude::*; #[component] pub(crate) fn Components() -> Element { - let segments: ReadOnlySignal> = Default::default(); - let query: ReadOnlySignal = Default::default(); + let segments: ReadSignal> = Default::default(); + let query: ReadSignal = Default::default(); fn format_segments(segments: &[String], query: &str) -> String { let segments = segments.join("/"); diff --git a/packages/docsite/src/components/nav.rs b/packages/docsite/src/components/nav.rs index 47b9bd027f..ba6ac14686 100644 --- a/packages/docsite/src/components/nav.rs +++ b/packages/docsite/src/components/nav.rs @@ -145,7 +145,7 @@ static LINKS: &[(&str, &str)] = &[ }, ), // ("SDK", "/sdk"), - // ("Playground", "/playground"), + ("Playground", "/playground"), ("Components", "/components"), // ("Awesome", "/awesome"), ("Blog", "/blog"), diff --git a/packages/docsite/src/components/playground.rs b/packages/docsite/src/components/playground.rs index 581e37f018..ad123b44b1 100644 --- a/packages/docsite/src/components/playground.rs +++ b/packages/docsite/src/components/playground.rs @@ -4,14 +4,14 @@ use dioxus_playground::PlaygroundUrls; #[cfg(not(feature = "production"))] const URLS: PlaygroundUrls = PlaygroundUrls { socket: "ws://localhost:3000/ws", - built: "http://localhost:3000/built/", - location: "http://localhost:8080", + server: "http://localhost:3000", + location: "http://localhost:8080/playground", }; #[cfg(feature = "production")] const URLS: PlaygroundUrls = PlaygroundUrls { - socket: "wss://docsite-playground.fly.dev/ws", - built: "https://docsite-playground.fly.dev/built/", + socket: "wss://docsite-playground-red-wildflower-209.fly.dev/ws", + server: "https://docsite-playground-red-wildflower-209.fly.dev", location: "https://dioxuslabs.com/playground", }; @@ -21,17 +21,11 @@ pub fn Playground(share_code: Option) -> Element { let mut on_client = use_signal(|| false); use_effect(move || on_client.set(true)); - // dioxus_playground::Playground { - // class: "playground-container max-w-screen-2xl mx-auto mt-8", - // urls: URLS, - // share_code, - // } - if on_client() { rsx! { ErrorBoundary { handle_error: move |err: ErrorContext| { - let errors = err.errors(); + let error = err.error().unwrap(); rsx! { div { class: "mx-auto mt-8 max-w-3/4", @@ -41,23 +35,18 @@ pub fn Playground(share_code: Option) -> Element { br {} - for error in errors { - p { class: "dark:text-white font-light text-ghdarkmetal", "{error:?}" } - br {} - } + p { class: "dark:text-white font-light text-ghdarkmetal", "{error:?}" } } } }, + dioxus_playground::Playground { + class: "playground-container max-w-screen-2xl mx-auto", + urls: URLS, + share_code, + } } } } else { rsx! {} } } - -#[component] -pub fn SharePlayground(share_code: String) -> Element { - rsx! { - Playground { share_code } - } -} diff --git a/packages/docsite/src/docs.rs b/packages/docsite/src/docs.rs index 0ced0a8b67..e13cdd8bd8 100644 --- a/packages/docsite/src/docs.rs +++ b/packages/docsite/src/docs.rs @@ -39,13 +39,15 @@ pub fn use_try_current_docs_version() -> Option { Route::Docs05 { child } => Some(CurrentDocsVersion::V05(child)), Route::Docs04 { child } => Some(CurrentDocsVersion::V04(child)), Route::Docs03 { child } => Some(CurrentDocsVersion::V03(child)), - Route::Homepage {} => None, - Route::Components { .. } => None, - Route::Awesome {} => None, - Route::Deploy {} => None, - Route::BlogList {} => None, - Route::BlogPost { .. } => None, - Route::Err404 { .. } => None, + Route::Homepage {} + | Route::Components { .. } + | Route::Awesome {} + | Route::Deploy {} + | Route::BlogList {} + | Route::BlogPost { .. } + | Route::Playground { .. } + | Route::SharePlayground { .. } + | Route::Err404 { .. } => None, } } diff --git a/packages/docsite/src/main.rs b/packages/docsite/src/main.rs index f9a771b6a8..809930d4d7 100644 --- a/packages/docsite/src/main.rs +++ b/packages/docsite/src/main.rs @@ -190,20 +190,15 @@ fn Head() -> Element { #[derive(Clone, Routable, PartialEq, Eq, Serialize, Deserialize, Debug)] #[rustfmt::skip] pub enum Route { - // #[layout(HeadLayout)] - // #[layout(HeaderLayout)] - // #[layout(FooterLayout)] #[layout(HeaderFooter)] #[route("/")] Homepage {}, - // #[route("/playground")] - // Playground {}, - - // #[route("/playground/shared/:share_code")] - // SharePlayground { share_code: String }, - + #[route("/playground")] + Playground {}, + #[route("/playground/shared/:share_code", Playground)] + SharePlayground { share_code: String }, #[route("/awesome")] Awesome {}, diff --git a/packages/include_mdbook/packages/mdbook-gen-example/build.rs b/packages/include_mdbook/packages/mdbook-gen-example/build.rs index eead20d545..4c1c4b1204 100644 --- a/packages/include_mdbook/packages/mdbook-gen-example/build.rs +++ b/packages/include_mdbook/packages/mdbook-gen-example/build.rs @@ -1,5 +1,3 @@ -use std::{env::current_dir, path::PathBuf}; - fn main() { // // re-run only if the "example-book" directory changes // println!("cargo:rerun-if-changed=example-book"); diff --git a/packages/include_mdbook/packages/mdbook-gen/src/lib.rs b/packages/include_mdbook/packages/mdbook-gen/src/lib.rs index c92496cb0c..f73311d8a5 100644 --- a/packages/include_mdbook/packages/mdbook-gen/src/lib.rs +++ b/packages/include_mdbook/packages/mdbook-gen/src/lib.rs @@ -8,7 +8,6 @@ use mdbook_shared::MdBook; use proc_macro2::Ident; use proc_macro2::Span; use proc_macro2::TokenStream as TokenStream2; -use quote::format_ident; use quote::quote; use quote::ToTokens; use syn::LitStr; @@ -24,8 +23,7 @@ pub fn make_docs_from_ws(version: &str) { let mut out = generate_router_build_script(mdbook_dir); out.push_str("use dioxus_docs_examples::*;\n"); out.push_str("use dioxus::prelude::*;\n"); - let version_flattened = version.replace(".", ""); - let filename = format!("docsgen.rs"); + let filename = "docsgen.rs".to_string(); std::fs::write(out_dir.join(filename), out).unwrap(); } @@ -330,20 +328,3 @@ pub(crate) fn path_to_route_enum_with_section( } }) } - -fn rustfmt_via_cli(input: &str) -> String { - let tmpfile = std::env::temp_dir().join(format!("mdbook-gen-{}.rs", std::process::id())); - std::fs::write(&tmpfile, input).unwrap(); - - let file = std::fs::File::open(&tmpfile).unwrap(); - let output = std::process::Command::new("rustfmt") - .arg("--edition=2021") - .stdin(file) - .stdout(std::process::Stdio::piped()) - .output() - .unwrap(); - - _ = std::fs::remove_file(tmpfile); - - String::from_utf8(output.stdout).unwrap() -} diff --git a/packages/include_mdbook/packages/mdbook-shared/src/query.rs b/packages/include_mdbook/packages/mdbook-shared/src/query.rs index 936e6bd413..480bd45f96 100644 --- a/packages/include_mdbook/packages/mdbook-shared/src/query.rs +++ b/packages/include_mdbook/packages/mdbook-shared/src/query.rs @@ -194,8 +194,7 @@ impl MdBook { c.is_ascii_alphanumeric() || *c == ' ' || *c == '-' || *c == '_' }) .collect::() - .replace('_', "-") - .replace(' ', "-"); + .replace(['_', ' '], "-"); sections.push(Section { level: *current_level as usize, title: title.clone(), diff --git a/packages/include_mdbook/packages/mdbook-shared/src/summary.rs b/packages/include_mdbook/packages/mdbook-shared/src/summary.rs index d5162d9f62..18b636b3f7 100644 --- a/packages/include_mdbook/packages/mdbook-shared/src/summary.rs +++ b/packages/include_mdbook/packages/mdbook-shared/src/summary.rs @@ -246,7 +246,7 @@ impl<'a> SummaryParser<'a> { /// Get the current line and column to give the user more useful error /// messages. fn current_location(&self) -> (usize, usize) { - let previous_text = self.src[..self.offset].as_bytes(); + let previous_text = &self.src.as_bytes()[..self.offset]; let line = Memchr::new(b'\n', previous_text).count() + 1; let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0); let col = self.src[start_of_line..self.offset].chars().count(); diff --git a/packages/notion-to-blog/src/main.rs b/packages/notion-to-blog/src/main.rs index a742885461..32b195b3f6 100644 --- a/packages/notion-to-blog/src/main.rs +++ b/packages/notion-to-blog/src/main.rs @@ -222,17 +222,16 @@ fn transform_markdown(content: &str, image_mapping: &HashMap) -> // Check if this is a video file - if so, convert to image syntax let url_decoded = dest_url.replace("%20", " "); if let Some(filename) = Path::new(&url_decoded).file_name().and_then(|s| s.to_str()) + && is_media_file(filename) { - if is_media_file(filename) { - // Convert link to image for video files - events.push(Event::Start(Tag::Image { - link_type, - dest_url: processed_url.into(), - title, - id, - })); - continue; - } + // Convert link to image for video files + events.push(Event::Start(Tag::Image { + link_type, + dest_url: processed_url.into(), + title, + id, + })); + continue; } // Regular link handling @@ -456,10 +455,10 @@ fn process_image_url(url: &str, image_mapping: &HashMap) -> Stri // Extract filename from URL let url_decoded = url.replace("%20", " "); - if let Some(filename) = Path::new(&url_decoded).file_name().and_then(|s| s.to_str()) { - if let Some(new_name) = image_mapping.get(filename) { - return format!("./assets/{}", new_name); - } + if let Some(filename) = Path::new(&url_decoded).file_name().and_then(|s| s.to_str()) + && let Some(new_name) = image_mapping.get(filename) + { + return format!("./assets/{}", new_name); } // Fallback: clean up the URL by removing URL encoding diff --git a/packages/playground/example-projects/playground-examples/calendar.rs b/packages/playground/example-projects/playground-examples/calendar.rs new file mode 100644 index 0000000000..149be2bcc1 --- /dev/null +++ b/packages/playground/example-projects/playground-examples/calendar.rs @@ -0,0 +1,53 @@ +//! A simple example showcasing the dx-components library. + +mod components; + +use components::calendar::*; +use dioxus::prelude::*; +use time::{macros::date, Date, UtcDateTime}; + +static THEME: Asset = asset!("/assets/dx-components-theme.css"); + +fn main() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + let mut selected_date = use_signal(|| None::); + let mut view_date = use_signal(|| UtcDateTime::now().date()); + rsx! { + document::Stylesheet { href: THEME } + div { + display: "flex", + align_items: "center", + justify_content: "center", + height: "100vh", + width: "100vw", + div { + width: "258px", + Calendar { + selected_date: selected_date(), + on_date_change: move |date| { + selected_date.set(date); + }, + view_date: view_date(), + on_view_change: move |new_view: Date| { + view_date.set(new_view); + }, + min_date: date!(1995 - 07 - 21), + max_date: date!(2035 - 09 - 11), + CalendarHeader { + CalendarNavigation { + CalendarPreviousMonthButton {} + CalendarSelectMonth {} + CalendarSelectYear {} + CalendarNextMonthButton {} + } + } + CalendarGrid {} + } + } + } + } +} diff --git a/packages/playground/example-projects/examples/counter.rs b/packages/playground/example-projects/playground-examples/counter.rs similarity index 100% rename from packages/playground/example-projects/examples/counter.rs rename to packages/playground/example-projects/playground-examples/counter.rs diff --git a/packages/playground/example-projects/examples/welcome.rs b/packages/playground/example-projects/playground-examples/welcome.rs similarity index 100% rename from packages/playground/example-projects/examples/welcome.rs rename to packages/playground/example-projects/playground-examples/welcome.rs diff --git a/packages/playground/example-projects/src/lib.rs b/packages/playground/example-projects/src/lib.rs index 2e9b351e23..dd03d7276b 100644 --- a/packages/playground/example-projects/src/lib.rs +++ b/packages/playground/example-projects/src/lib.rs @@ -1,73 +1,74 @@ -// use include_dir::DirEntry; -// use model::Project; -// use once_cell::sync::Lazy; - -// static EXAMPLES: include_dir::Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/examples"); - -// pub fn get_welcome_project() -> Project { -// get_example_projects() -// .iter() -// .find(|p| &p.path == "welcome.rs") -// .unwrap() -// .clone() -// } - -// /// Returns a list of all example projects. -// pub fn get_example_projects() -> &'static [Project] { -// static LIST: Lazy> = once_cell::sync::Lazy::new(|| { -// let mut projects = Vec::new(); - -// for entry in EXAMPLES.entries() { -// let DirEntry::File(entry) = entry else { -// continue; -// }; - -// let path = entry.path(); -// let contents = entry.contents(); -// let contents = String::from_utf8(contents.to_vec()).unwrap(); - -// let mut description = String::new(); - -// for line in contents.lines() { -// if let Some(line) = line.strip_prefix("//!") { -// description.push_str(line); -// description.push('\n'); -// } else { -// break; -// } -// } - -// // Remove the trailing newline -// description.pop(); - -// let mut project = Project::new( -// contents, -// Some(description), -// Some(path.to_string_lossy().to_string()), -// ); - -// project.prebuilt = true; - -// projects.push(project); -// } - -// projects -// }); - -// LIST.as_ref() -// } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn has_projects() { -// assert!(!dbg!(get_example_projects()).is_empty()); -// } - -// #[test] -// fn has_welcome() { -// dbg!(get_welcome_project()); -// } -// } +use include_dir::DirEntry; +use model::Project; +use once_cell::sync::Lazy; + +static EXAMPLES: include_dir::Dir = + include_dir::include_dir!("$CARGO_MANIFEST_DIR/playground-examples"); + +pub fn get_welcome_project() -> Project { + get_example_projects() + .iter() + .find(|p| &p.path == "welcome.rs") + .unwrap() + .clone() +} + +/// Returns a list of all example projects. +pub fn get_example_projects() -> &'static [Project] { + static LIST: Lazy> = once_cell::sync::Lazy::new(|| { + let mut projects = Vec::new(); + + for entry in EXAMPLES.entries() { + let DirEntry::File(entry) = entry else { + continue; + }; + + let path = entry.path(); + let contents = entry.contents(); + let contents = String::from_utf8(contents.to_vec()).unwrap(); + + let mut description = String::new(); + + for line in contents.lines() { + if let Some(line) = line.strip_prefix("//!") { + description.push_str(line); + description.push('\n'); + } else { + break; + } + } + + // Remove the trailing newline + description.pop(); + + let mut project = Project::new( + contents, + Some(description), + Some(path.to_string_lossy().to_string()), + ); + + project.prebuilt = true; + + projects.push(project); + } + + projects + }); + + LIST.as_ref() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn has_projects() { + assert!(!dbg!(get_example_projects()).is_empty()); + } + + #[test] + fn has_welcome() { + dbg!(get_welcome_project()); + } +} diff --git a/packages/playground/model/Cargo.toml b/packages/playground/model/Cargo.toml index c249232017..cc1fe0255d 100644 --- a/packages/playground/model/Cargo.toml +++ b/packages/playground/model/Cargo.toml @@ -19,6 +19,7 @@ axum = { workspace = true, features = ["ws"], optional = true } gloo-net = { workspace = true, optional = true } gloo-utils = { workspace = true, optional = true } dioxus-document = { workspace = true, optional = true } +dioxus-devtools.workspace = true [features] server = ["dep:dioxus-dx-wire-format", "dep:axum"] diff --git a/packages/playground/model/src/api.rs b/packages/playground/model/src/api.rs index 40ca844d43..d6c5484d27 100644 --- a/packages/playground/model/src/api.rs +++ b/packages/playground/model/src/api.rs @@ -11,13 +11,13 @@ pub struct ShareProjectReq { /// API response for sharing a project. #[derive(Debug, Serialize, Deserialize)] pub struct ShareProjectRes { - pub id: String, + pub id: uuid::Uuid, } /// API response for requesting a shared project. #[derive(Debug, Serialize, Deserialize)] pub struct GetSharedProjectRes { - pub id: String, + pub id: uuid::Uuid, pub code: String, } diff --git a/packages/playground/model/src/lib.rs b/packages/playground/model/src/lib.rs index 6e571acfbf..730536afd4 100644 --- a/packages/playground/model/src/lib.rs +++ b/packages/playground/model/src/lib.rs @@ -1,128 +1,154 @@ -// use serde::{Deserialize, Serialize}; -// use std::error::Error; -// use std::string::FromUtf8Error; -// use thiserror::Error; -// use uuid::Uuid; - -// pub mod api; - -// mod project; -// pub use project::Project; - -// #[cfg(feature = "server")] -// mod server; - -// #[cfg(feature = "web")] -// mod web; - -// #[derive(Debug, Serialize, Deserialize)] -// pub enum SocketMessage { -// BuildRequest(String), -// BuildFinished(Result), -// BuildStage(BuildStage), -// BuildDiagnostic(CargoDiagnostic), -// QueuePosition(usize), -// AlreadyConnected, -// } - -// /// A stage of building from the playground. -// #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -// pub enum BuildStage { -// Compiling { -// crates_compiled: usize, -// total_crates: usize, -// current_crate: String, -// }, -// RunningBindgen, -// Other, -// } - -// impl SocketMessage { -// pub fn as_json_string(&self) -> Result { -// Ok(serde_json::to_string(self)?) -// } -// } - -// impl TryFrom for SocketMessage { -// type Error = SocketError; - -// fn try_from(value: String) -> Result { -// Ok(serde_json::from_str(&value)?) -// } -// } - -// /// A cargo diagnostic -// #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -// pub struct CargoDiagnostic { -// pub target_crate: String, -// pub level: CargoLevel, -// pub message: String, -// pub spans: Vec, -// } - -// #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -// pub enum CargoLevel { -// Error, -// Warning, -// } - -// #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -// pub struct CargoDiagnosticSpan { -// pub is_primary: bool, -// pub line_start: usize, -// pub line_end: usize, -// pub column_start: usize, -// pub column_end: usize, -// pub label: Option, -// } - -// /// Any socket error. -// #[derive(Debug, Error)] -// #[non_exhaustive] -// pub enum SocketError { -// #[error(transparent)] -// ParseJson(#[from] serde_json::Error), - -// #[error(transparent)] -// Utf8Decode(#[from] FromUtf8Error), - -// #[cfg(feature = "web")] -// #[error(transparent)] -// Gloo(#[from] gloo_net::websocket::WebSocketError), - -// #[cfg(feature = "server")] -// #[error(transparent)] -// Axum(#[from] axum::Error), -// } - -// /// Generic App Error -// #[derive(Debug, Error)] -// #[non_exhaustive] -// pub enum AppError { -// #[error("parse error: {0}")] -// Parse(Box), - -// #[error(transparent)] -// Request(#[from] reqwest::Error), - -// #[error("build is already running")] -// BuildIsAlreadyRunning, - -// #[error("resource not found")] -// ResourceNotFound, - -// // Web-specific errors -// #[cfg(feature = "web")] -// #[error(transparent)] -// Socket(#[from] SocketError), - -// #[cfg(feature = "web")] -// #[error(transparent)] -// Js(Box), -// } - -// impl From for AppError { -// fn from(value: serde_json::Error) -> Self { -// Self::Parse(Box::new(value)) -// } -// } +use dioxus_devtools::subsecond::JumpTable; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::string::FromUtf8Error; +use std::time::Duration; +use thiserror::Error; +use uuid::Uuid; + +pub mod api; + +mod project; +pub use project::Project; + +#[cfg(feature = "server")] +mod server; + +#[cfg(feature = "web")] +mod web; + +/// The result of a build +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BuildResult { + /// The project was built and is now available under the uuid + Built(Uuid), + /// The project was hotpatched + HotPatched(JumpTable), + /// The build failed with an error message + Failed(String), +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum SocketMessage { + BuildRequest { + code: String, + previous_build_id: Option, + }, + BuildFinished(BuildResult), + BuildStage(BuildStage), + BuildDiagnostic(CargoDiagnostic), + QueuePosition(usize), + RateLimited(Duration), + AlreadyConnected, +} + +impl SocketMessage { + /// Check if the socket message is the finished variant + pub fn is_finished(&self) -> bool { + matches!(self, SocketMessage::BuildFinished(_)) + } +} + +/// A stage of building from the playground. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BuildStage { + Compiling { + crates_compiled: usize, + total_crates: usize, + current_crate: String, + }, + RunningBindgen, + Other, +} + +impl SocketMessage { + pub fn as_json_string(&self) -> Result { + Ok(serde_json::to_string(self)?) + } +} + +impl TryFrom for SocketMessage { + type Error = SocketError; + + fn try_from(value: String) -> Result { + Ok(serde_json::from_str(&value)?) + } +} + +/// A cargo diagnostic +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] +pub struct CargoDiagnostic { + pub target_crate: Option, + pub level: CargoLevel, + pub message: String, + pub spans: Vec, + pub rendered: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] +pub enum CargoLevel { + Error, + Warning, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] +pub struct CargoDiagnosticSpan { + pub is_primary: bool, + pub line_start: usize, + pub line_end: usize, + pub column_start: usize, + pub column_end: usize, + pub label: Option, + pub file_name: String, +} + +/// Any socket error. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum SocketError { + #[error(transparent)] + ParseJson(#[from] serde_json::Error), + + #[error(transparent)] + Utf8Decode(#[from] FromUtf8Error), + + #[cfg(feature = "web")] + #[error(transparent)] + Gloo(#[from] gloo_net::websocket::WebSocketError), + + #[cfg(feature = "server")] + #[error(transparent)] + Axum(#[from] axum::Error), +} + +/// Generic App Error +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum AppError { + #[error("parse error: {0}")] + Parse(Box), + + #[error(transparent)] + Request(#[from] reqwest::Error), + + #[error("build is already running")] + BuildIsAlreadyRunning, + + #[error("resource not found")] + ResourceNotFound, + + // Web-specific errors + #[cfg(feature = "web")] + #[error(transparent)] + Socket(#[from] SocketError), + + #[cfg(feature = "web")] + #[error(transparent)] + Js(Box), +} + +impl From for AppError { + fn from(value: serde_json::Error) -> Self { + Self::Parse(Box::new(value)) + } +} diff --git a/packages/playground/model/src/project.rs b/packages/playground/model/src/project.rs index bbe63c7435..754969335c 100644 --- a/packages/playground/model/src/project.rs +++ b/packages/playground/model/src/project.rs @@ -13,7 +13,7 @@ pub struct Project { contents: String, pub prebuilt: bool, id: Uuid, - shared_id: Option, + shared_id: Option, } impl Project { @@ -36,6 +36,14 @@ impl Project { self.id } + pub fn shared_id(&self) -> Option { + self.shared_id + } + + pub fn set_shared_id(&mut self, new_shared_id: Uuid) { + self.shared_id = Some(new_shared_id); + } + pub fn contents(&self) -> String { self.contents.clone() } @@ -68,24 +76,25 @@ impl Project { }) } - pub async fn share_project(&mut self, client: &ApiClient) -> Result { + pub async fn share_project( + shared_id: Option, + code: String, + client: &ApiClient, + ) -> Result { // If the project has already been shared, return the share code. // We remove the shared id if the content changes. - if let Some(share_code) = &self.shared_id { - return Ok(share_code.clone()); + if let Some(share_code) = &shared_id { + return Ok(*share_code); } let url = format!("{}/shared", client.server_url); let res = client .post(url) - .json(&ShareProjectReq { - code: self.contents.clone(), - }) + .json(&ShareProjectReq { code }) .send() .await?; let res = res.json::().await?; - self.shared_id = Some(res.id.clone()); Ok(res.id) } diff --git a/packages/playground/model/src/server.rs b/packages/playground/model/src/server.rs index 5d154851f8..217e05e4e0 100644 --- a/packages/playground/model/src/server.rs +++ b/packages/playground/model/src/server.rs @@ -6,6 +6,7 @@ use crate::{ }; use axum::http::StatusCode; use axum::{extract::ws, response::IntoResponse}; +use dioxus_dx_wire_format::cargo_metadata::diagnostic::{Diagnostic, DiagnosticSpan}; use dioxus_dx_wire_format::{ cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage}, BuildStage as DxBuildStage, @@ -36,7 +37,7 @@ impl SocketMessage { let msg = self .as_json_string() .expect("socket message should be valid json"); - ws::Message::Text(msg) + ws::Message::Text(msg.into()) } } @@ -44,8 +45,8 @@ impl TryFrom for SocketMessage { type Error = SocketError; fn try_from(value: ws::Message) -> Result { - let text = value.into_text()?; - SocketMessage::try_from(text) + let text = value.into_data(); + Ok(serde_json::from_slice(&text)?) } } @@ -65,28 +66,51 @@ impl TryFrom for CargoDiagnostic { let message = diagnostic.message; // Collect spans - let spans = diagnostic - .spans - .iter() - .map(|s| CargoDiagnosticSpan { - is_primary: s.is_primary, - line_start: s.line_start, - line_end: s.line_end, - column_start: s.column_start, - column_end: s.column_end, - label: s.label.clone(), - }) - .collect(); + let spans = diagnostic.spans.iter().map(|s| s.clone().into()).collect(); Ok(Self { - target_crate: value.target.name, + target_crate: Some(value.target.name), level, message, spans, + rendered: diagnostic.rendered, }) } } +impl From for CargoDiagnostic { + fn from(value: Diagnostic) -> Self { + let level = CargoLevel::Error; + + let message = value.message; + + // Collect spans + let spans = value.spans.iter().map(|s| s.clone().into()).collect(); + + Self { + target_crate: None, + level, + message, + spans, + rendered: value.rendered, + } + } +} + +impl From for CargoDiagnosticSpan { + fn from(value: DiagnosticSpan) -> Self { + Self { + is_primary: value.is_primary, + line_start: value.line_start, + line_end: value.line_end, + column_start: value.column_start, + column_end: value.column_end, + label: value.label, + file_name: value.file_name, + } + } +} + /// IntoResponse for app errors. impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { diff --git a/packages/playground/playground/Cargo.toml b/packages/playground/playground/Cargo.toml index 89562b9e4c..5130f3c621 100644 --- a/packages/playground/playground/Cargo.toml +++ b/packages/playground/playground/Cargo.toml @@ -14,9 +14,9 @@ uuid = { workspace = true } thiserror = { workspace = true } # Dioxus -dioxus = { workspace = true, features = ["web"] } +dioxus = { workspace = true, features = ["web", "router"] } dioxus-document = { workspace = true } -# dioxus-sdk = { workspace = true, features = [ "window_size", "timing", ] } +# dioxus-sdk = { workspace = true, features = ["util", "time", "window"] } # Hot reload / Paste as RSX dioxus-core = { workspace = true } @@ -31,15 +31,22 @@ dioxus-rsx-rosetta = { workspace = true } dioxus-autofmt = { workspace = true } gloo-utils = { workspace = true } +gloo-timers = { version = "0.3.0" } wasm-bindgen = { version = "0.2.99", features = ["serde-serialize"] } miniz_oxide = { version = "0.8.0", features = ["std"] } base64 = "0.22.1" +ansi-parser = "0.9.1" syn = { workspace = true } proc-macro2 = "1.0.89" - example-projects = { workspace = true } +dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } +tracing = "0.1.41" + +[target.'cfg(target_family = "wasm")'.dependencies] +web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList"] } + [target.'cfg(target_arch = "wasm32")'.dependencies] # dioxus-sdk = { workspace = true, default-features = false, features = ["system_theme", "window_size", "timing",] } diff --git a/packages/playground/playground/assets/dx-components-theme.css b/packages/playground/playground/assets/dx-components-theme.css new file mode 100644 index 0000000000..7c61d73540 --- /dev/null +++ b/packages/playground/playground/assets/dx-components-theme.css @@ -0,0 +1,83 @@ +/* This file contains the global styles for the styled dioxus components. You only + * need to import this file once in your project root. + */ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +body { + padding: 0; + margin: 0; + background-color: var(--primary-color); + color: var(--secondary-color-4); + font-family: Inter, sans-serif; + font-optical-sizing: auto; + font-style: normal; + font-weight: 400; +} + +@media (prefers-color-scheme: dark) { + :root { + --dark: initial; + --light: ; + } +} + +@media (prefers-color-scheme: light) { + :root { + --dark: ; + --light: initial; + } +} + +:root { + /* Primary colors */ + --primary-color: var(--dark, #000) var(--light, #fff); + --primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb); + --primary-color-2: var(--dark, #0a0a0a) var(--light, #fff); + --primary-color-3: var(--dark, #141313) var(--light, #f8f8f8); + --primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8); + --primary-color-5: var(--dark, #262626) var(--light, #f5f5f5); + --primary-color-6: var(--dark, #232323) var(--light, #e5e5e5); + --primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0); + + /* Secondary colors */ + --secondary-color: var(--dark, #fff) var(--light, #000); + --secondary-color-1: var(--dark, #fafafa) var(--light, #000); + --secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d); + --secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b); + --secondary-color-4: var(--dark, #d4d4d4) var(--light, #111); + --secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484); + --secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0); + + /* Highlight colors */ + --focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff); + --primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5); + --secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981); + --primary-warning-color: var(--dark, #342203) var(--light, #fffbeb); + --secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b); + --primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626); + --secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444); + --contrast-error-color: var(--dark, var(--secondary-color-3)) + var(--light, var(--primary-color)); + --primary-info-color: var(--dark, var(--primary-color-5)) + var(--light, var(--primary-color)); + --secondary-info-color: var(--dark, var(--primary-color-7)) + var(--light, var(--secondary-color-3)); +} + +/* Modern browsers with `scrollbar-*` support */ +@supports (scrollbar-width: auto) { + :not(:hover) { + scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%); + } + + :hover { + scrollbar-color: var(--secondary-color-2) rgba(0, 0, 0, 0); + } +} + +/* Legacy browsers with `::-webkit-scrollbar-*` support */ +@supports selector(::-webkit-scrollbar) { + :root::-webkit-scrollbar-track { + background: transparent; + } +} diff --git a/packages/playground/playground/assets/dxp.css b/packages/playground/playground/assets/dxp.css index 8b1a7b0b27..be2302366a 100644 --- a/packages/playground/playground/assets/dxp.css +++ b/packages/playground/playground/assets/dxp.css @@ -1,649 +1,199 @@ -/* Variables and their common uses */ -:root { - /* Light Theme */ - --dxp-bg-light-darker: white; - --dxp-bg-light: white; - --dxp-bg-light-lighter: white; - /* --dxp-bg-light-darker: #C1C6D2; */ - /* --dxp-bg-light: #DCDFE5; - --dxp-bg-light-lighter: #EDEFF2; */ - --dxp-bg-light-lighter-alt: #D6DAE1; - - --dxp-border-light: #b4b4b4; - --dxp-border-light-lighter: #dbdbdc; - /* --dxp-border-light: #15181E; - --dxp-border-light-lighter: #242933; */ - - --dxp-text-light: #131313; - /* --dxp-text-light: #131313; */ - - --dxp-log-info-light: #002fff; - --dxp-log-warn-light: #ff8400; - --dxp-log-error-light: #ff0000; - - /* Dark Theme */ - --dxp-bg-dark-darker: #21252E; - --dxp-bg-dark: #000000; - --dxp-bg-dark-lighter: #454E61; - --dxp-bg-dark-lighter-alt: #363E4D; - - --dxp-border-dark: #5B667D; - --dxp-border-dark-lighter: #8292B2; - - --dxp-text-dark: white; - /* --dxp-text-dark: #dfdfdf; */ - - --dxp-log-info-dark: #4fa2f0; - --dxp-log-warn-dark: #f0b04f; - --dxp-log-error-dark: #f04f4f; - - /* Both Themes */ - --dxp-log-border: #374155; -} - #dxp-playground-root { - border: 1px solid var(--dxp-border-light); - overflow: hidden; + overflow: hidden; } /* Header */ #dxp-header { - border-bottom: 1px solid var(--dxp-border-light); - background-color: var(--dxp-bg-light-darker); - height: 36px; - display: flex; - flex-direction: row; - margin-left: auto; - margin-right: auto; - width: 100%; + display: flex; + flex-direction: row; + padding: 0.5rem; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); } #dxp-header-left { - flex-grow: 1; - display: flex; - flex-direction: row; - align-items: center; - padding: 8px; + flex-grow: 1; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; } #dxp-header-left-divider { - flex-grow: 1; + flex-grow: 1; } #dxp-header-right { - flex-grow: 1; - min-width: 100px; - display: flex; - flex-direction: row; - justify-content: end; - align-items: center; - padding: 8px; + flex-grow: 1; + min-width: 100px; + display: flex; + flex-direction: row; + justify-content: end; + align-items: center; + gap: 0.5rem; } - /* Header buttons */ .dxp-ctrl-btn { - height: 25px; - padding: 0px 10px; - border-radius: 5px; - border-style: none; - color: var(--dxp-text-light); - font-family: "Inter", sans-serif; - font-size: 14px; - font-weight: 400; - letter-spacing: -0.06em; - transition: background-color 0.2s ease; + height: 25px; + padding: 0px 10px; + border-radius: 5px; + border-style: none; + font-family: "Inter", sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: -0.06em; + transition: background-color 0.2s ease; } -#dxp-menu-btn { - background-color: var(--dxp-bg-light-lighter); - display: flex; - align-items: center; - justify-content: center; - padding: 5px; - +#dxp-header-share-btn { + margin-left: 8px; + width: 8rem; } - -#dxp-menu-btn:hover { - cursor: pointer; - background-color: var(--dxp-bg-light-lighter-alt); -} - -#dxp-menu-btn.dxp-open { - border: 1px solid var(--dxp-border-light-lighter); -} - -#dxp-menu-btn>svg { - color: var(--dxp-text-light); +@media (max-width: 600px) { + #dxp-header-share-btn { + width: 6rem; + padding-left: 0.25rem; + padding-right: 0.25rem; + } } .dxp-ctrl-btn:hover { - cursor: pointer; - background-color: var(--dxp-bg-light-lighter-alt); -} - -#dxp-header-left>.dxp-ctrl-btn:not(:first-child) { - margin-left: 12px; -} - -#dxp-header-right>.dxp-ctrl-btn:not(:last-child) { - margin-right: 12px; -} - -#dxp-run-btn { - background-color: var(--dxp-bg-light-lighter); - color: var(--dxp-text-light); - display: flex; - flex-direction: row; - align-items: center; - justify-items: start; - font-family: "Inter", sans-serif; - font-size: 14px; - font-weight: 400; - letter-spacing: -0.06em; -} - -#dxp-run-btn:not(.disabled) { - background-color: #288AE5; - color: var(--dxp-text-dark); -} - -#dxp-run-btn.disabled:hover { - cursor: not-allowed; -} - -#dxp-run-btn:hover:not(.disabled) { - background-color: #1E68AD; -} - -#dxp-run-btn>svg { - color: var(--dxp-text-light); - height: 16px; - position: relative; - left: -5px; -} - -.dxp-file-btn { - min-width: 100px; - background-color: var(--dxp-bg-light-lighter); -} - -.dxp-selected-file { - border: 1px solid var(--dxp-border-light-lighter); -} - -#dxp-share-btn { - background-color: transparent; -} - -#dxp-share-btn:hover { - text-decoration: underline; -} - -#dxp-examples-list { - width: 200px; - display: none; - background-color: var(--dxp-bg-light-darker); -} - -#dxp-examples-list.dxp-open { - display: block; -} - - -.dxp-example-project { - color: var(--dxp-text-light); - background: inherit; - text-align: left; - cursor: pointer; - width: 100%; - padding: 10px 10px; - margin: 0; - transition: background-color 0.2s ease; - border: none; - border-bottom: 1px solid var(--dxp-bg-light-lighter-alt); -} - -.dxp-example-project:hover { - background-color: var(--dxp-bg-light-lighter-alt); + cursor: pointer; } -.dxp-example-project>h3 { - padding: 0; - margin: 0; - font-family: "Inter", sans-serif; - font-weight: 500; +#dxp-header-left > .dxp-ctrl-btn:not(:first-child) { + margin-left: 12px; } -.dxp-example-project>p { - font-family: "Inter", sans-serif; - font-weight: 300; +#dxp-header-right > .dxp-ctrl-btn:not(:last-child) { + margin-right: 12px; } #dxp-lower-half { - display: flex; - flex-grow: 1; + display: flex; + flex-grow: 1; } /* Panes */ #dxp-panes { - flex-grow: 1; - flex-direction: row; - margin-left: auto; - margin-right: auto; - display: flex; - border-left: 1px solid var(--dxp-border-light); + flex-grow: 1; + flex-direction: row; + margin-left: auto; + margin-right: auto; + display: flex; } #dxp-panes-left { - width: 50%; - min-width: 100px; - background-color: var(--dxp-bg-light); + width: 50%; + min-width: 100px; } #dxp-panes-draggable { - width: 3px; - background-color: var(--dxp-border-light); - cursor: col-resize; - user-select: none; + width: 12px; + cursor: col-resize; + user-select: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } - #dxp-panes-right { - display: flex; - width: 50%; - min-width: 100px; - background-color: var(--dxp-bg-light-darker); - position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 50%; + min-width: 100px; + position: relative; + overflow-y: scroll; } -#dxp-panes-right>p { - color: var(--dxp-text-light); - font-family: "Inter", sans-serif; - user-select: none; - text-align: center; - width: 100%; - height: fit-content; - top: 30%; - position: relative; +#dxp-panes-right > p { + font-family: "Inter", sans-serif; + user-select: none; + text-align: center; + width: 100%; + height: fit-content; + top: 30%; + position: relative; } -#dxp-panes-right>#iframe-cover { - width: 100%; - height: 100%; - top: 0; - left: 0; - position: absolute; - opacity: 1; - pointer-events: all; +#dxp-panes-right > #iframe-cover { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 1; + pointer-events: all; } -#dxp-panes-right>#dxp-viewport { - width: 100%; - margin: 10px; - background-color: white; +#dxp-panes-right > #dxp-viewport { + width: 100%; + height: 100%; + background-color: white; + color: black; } #dxp-iframe { - width: 100%; - height: 100%; - border: none; -} - -#dxp-panes-right>#dxp-examples-viewport { - padding: 8px; - width: 100%; - height: 100%; -} - -#dxp-panes-right>#dxp-progress-container { - display: flex; - flex-direction: column; - height: fit-content; - width: 100%; - top: 30%; - position: relative; -} - -#dxp-panes-right>#dxp-progress-container>p { - color: var(--dxp-text-light); - font-family: "Inter", sans-serif; - margin-left: auto; - margin-right: auto; - width: 70%; - user-select: none; -} - -#dxp-panes-right>#dxp-progress-container>#dxp-progress { - height: 6px; - width: 70%; - margin-left: auto; - margin-right: auto; - background-color: var(--dxp-bg-light-lighter); - border-radius: 5px; - overflow: hidden; -} - -#dxp-panes-right>#dxp-progress-container>#dxp-progress>#dxp-bar { - background: #288AE5; - background: linear-gradient(90deg, rgb(91, 87, 202) 0%, rgba(40, 138, 229, 1) 100%); - height: 100%; -} - -/* Modal */ - -#dxp-modal-bg { - background-color: rgba(0, 0, 0, 40%); - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 100; - - display: flex; - justify-content: center; - align-items: center; + width: 100%; + height: 100%; + border: none; } -#dxp-modal { - background-color: var(--dxp-bg-light); - border: 1px solid #000000; - color: var(--dxp-text-light); - border-radius: 5px; - padding: 15px; - margin-bottom: 20vh; - - font-family: "Inter", sans-serif; - font-weight: 400; - - max-width: 500px; +#dxp-panes-right > #dxp-examples-viewport { + padding: 8px; + width: 100%; + height: 100%; } -#dxp-modal-header { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; +#dxp-panes-right > #dxp-progress-container { + display: flex; + flex-direction: column; + height: fit-content; + width: 100%; + top: 30%; + position: relative; } -#dxp-modal-header>svg { - height: 40px; +#dxp-panes-right > #dxp-progress-container > p { + font-family: "Inter", sans-serif; + margin-left: auto; + margin-right: auto; + width: 70%; + user-select: none; } -#dxp-modal-title { - font-size: 20px; - font-weight: 600; - margin-top: 0px; - margin-bottom: 0px; - margin-left: 10px; +#dxp-panes-right > #dxp-progress-container > #dxp-progress { + height: 6px; + width: 70%; + margin-left: auto; + margin-right: auto; + border-radius: 5px; + overflow: hidden; } -#dxp-modal-text { - font-weight: 400; - font-size: 16px; - margin-bottom: 20px; +#dxp-panes-right > #dxp-progress-container > #dxp-progress > #dxp-bar { + background: #288ae5; + background: linear-gradient( + 90deg, + rgb(91, 87, 202) 0%, + rgba(40, 138, 229, 1) 100% + ); + height: 100%; } -#dxp-modal-ok-btn { - display: block; - background-color: var(--dxp-bg-light-lighter); - border-radius: 3px; - color: var(--dxp-text-light); - padding: 8px; - - font-weight: 400; - font-size: 14px; - margin-left: auto; - - border: none; - transition: background-color 0.2s ease; -} -#dxp-modal-ok-btn:hover { - background-color: var(--dxp-bg-light-darker); - cursor: pointer; -} /* Logs pane */ #logs { - display: flex; - flex-direction: column; - width: 100%; - overflow-y: auto; -} - -#logs .log { - padding: 20px 15px; - transition: background-color 0.3s ease; - color: var(--dxp-text-light); - border-bottom: var(--dxp-log-border) 1px solid; -} - -#logs .log:hover { - background-color: var(--dxp-bg-light); -} - -#logs .log .log-level { - font-family: "Inter", sans-serif; - padding-top: 0px; - padding-bottom: 10px; - margin: 0px; -} - -#logs .log .level-error { - color: var(--dxp-log-error-light); -} - -#logs .log .level-warn { - color: var(--dxp-log-warn-light); -} - -#logs .log .level-info { - color: var(--dxp-log-info-light) -} - -#logs .log .log-codeblock { - background-color: var(--dxp-bg-light); - padding: 10px; - border-radius: 10px; - border: var(--dxp-border-light) 1px solid; -} - -#logs .log .log-message, -#logs.log.log-span { - font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; -} - -#logs .log .log-message { - padding: 0px; - margin: 0px; -} - -#logs .log .log-span { - padding: 0px; - margin: 0px; - font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; -} - -/* Media Queries */ -/* TODO: Fix right pane responsive design */ -/* @media screen and (max-width: 1000px) { - #dxp-header-right { - width: auto; - margin-left: auto; - } - - #dxp-header-left { - width: auto; - } - - #dxp-panes { + display: flex; flex-direction: column; - height: auto; - } - - #dxp-panes-left { width: 100%; - } - - #dxp-panes-draggable { - visibility: hidden; - display: none; - } - - #dxp-panes-right { - width: 100%; - min-height: 400px; - } -} */ - -/* Color Scheme Queries */ - -/* @media screen and (max-width: 1000px) and (prefers-color-scheme: dark) { - #dxp-panes-right { - border-top: 1px solid var(--dxp-border-dark); - } -} */ - -@media screen and (prefers-color-scheme: dark) { - - #dxp-playground-root { - border-color: var(--dxp-border-dark); - } - - /* Header */ - #dxp-header { - border-bottom-color: var(--dxp-border-dark); - background-color: var(--dxp-bg-dark-darker); - } - - .dxp-ctrl-btn { - color: var(--dxp-text-dark); - } - - #dxp-menu-btn { - background-color: var(--dxp-bg-dark-lighter); - } - - #dxp-menu-btn:hover { - background-color: var(--dxp-bg-dark-lighter-alt); - } - - #dxp-menu-btn.dxp-open { - border: 1px solid var(--dxp-border-dark-lighter); - } - - #dxp-menu-btn>svg { - color: var(--dxp-text-dark); - } - - #dxp-run-btn { - color: var(--dxp-text-dark); - background-color: var(--dxp-bg-dark-lighter); - } - - #dxp-run-btn>svg { - color: var(--dxp-text-dark); - } - - .dxp-ctrl-btn:hover { - cursor: pointer; - background-color: var(--dxp-bg-dark-lighter-alt); - } - - .dxp-file-btn { - background-color: var(--dxp-bg-dark-lighter); - } - - .dxp-selected-file { - border-color: var(--dxp-border-dark-lighter); - } - - /* Examples */ - #dxp-examples-list { - background-color: var(--dxp-bg-dark-darker); - } - - .dxp-example-project { - color: var(--dxp-text-dark); - border-bottom: 1px solid var(--dxp-bg-dark-lighter-alt); - } - - .dxp-example-project:hover { - background-color: var(--dxp-bg-dark-lighter-alt); - } - - - /* Panes */ - - #dxp-panes { - border-left-color: var(--dxp-border-dark); - } - - #dxp-panes-left { - background-color: var(--dxp-bg-dark); - } - - #dxp-panes-draggable { - background-color: var(--dxp-border-dark); - } - - #dxp-panes-right { - background-color: var(--dxp-bg-dark-darker); - } - - #dxp-panes-right>p, - #dxp-panes-right>#dxp-progress-container>p { - color: var(--dxp-text-dark); - } - - #dxp-panes-right>#dxp-progress-container>#dxp-progress { - background-color: var(--dxp-bg-dark-lighter); - } - - /* Modal */ - - #dxp-modal { - background-color: var(--dxp-bg-dark); - border: 1px solid var(--dxp-border-dark); - color: var(--dxp-text-dark); - } - - #dxp-modal-ok-btn { - background-color: var(--dxp-bg-dark-lighter); - color: var(--dxp-text-dark); - } - - #dxp-modal-ok-btn:hover { - background-color: var(--dxp-bg-dark-lighter-alt); - } - - /* Logs */ - - #logs .log { - color: var(--dxp-text-dark); - } - - #logs .log:hover { - background-color: var(--dxp-bg-dark); - } - - #logs .log .level-error { - color: var(--dxp-log-error-dark); - } - - #logs .log .level-warn { - color: var(--dxp-log-warn-dark); - } - - #logs .log .level-info { - color: var(--dxp-log-info-dark) - } - - #logs .log .log-codeblock { - background-color: var(--dxp-bg-dark); - border-color: var(--dxp-border-dark); - } + overflow-y: auto; +} } diff --git a/packages/playground/playground/src/build.rs b/packages/playground/playground/src/build.rs index aaee788927..58562e4725 100644 --- a/packages/playground/playground/src/build.rs +++ b/packages/playground/playground/src/build.rs @@ -1,19 +1,26 @@ +use std::time::Duration; + use crate::ws; use dioxus::prelude::*; -use model::{AppError, CargoDiagnostic, SocketMessage}; +use model::{AppError, CargoDiagnostic, Project, SocketMessage}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Store)] pub(crate) enum BuildStage { NotStarted, Starting, + Waiting(Duration), + Queued(usize), Building(model::BuildStage), Finished(Result), } impl BuildStage { pub fn is_running(&self) -> bool { - matches!(self, Self::Starting | Self::Building(..)) + matches!( + self, + Self::Starting | Self::Building(..) | Self::Waiting(..) | Self::Queued(..) + ) } pub fn is_finished(&self) -> bool { @@ -39,92 +46,79 @@ impl BuildStage { None } - - /// Extract the compiling stage info if available. - pub fn get_compiling_stage(&self) -> Option<(usize, usize, String)> { - if let Self::Building(model::BuildStage::Compiling { - crates_compiled, - total_crates, - current_crate, - }) = self - { - return Some((*crates_compiled, *total_crates, current_crate.to_string())); - } - - None - } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Store)] pub(crate) struct BuildState { - stage: Signal, - queue_position: Signal>, - diagnostics: Signal>, + stage: BuildStage, + diagnostics: Vec, + previous_build_id: Option, } impl BuildState { - pub fn new() -> Self { + pub fn new(project: &Project) -> Self { Self { - stage: Signal::new(BuildStage::NotStarted), - queue_position: Signal::new(None), - diagnostics: Signal::new(Vec::new()), + stage: if project.prebuilt { + BuildStage::Finished(Ok(project.id())) + } else { + BuildStage::NotStarted + }, + diagnostics: Vec::new(), + previous_build_id: None, } } +} +#[store(pub)] +impl Store { /// Reset the build state to it's default. - pub fn reset(&mut self) { - self.stage.set(BuildStage::NotStarted); - self.queue_position.set(None); - self.diagnostics.clear(); + fn reset(&mut self) { + self.stage().set(BuildStage::NotStarted); + self.diagnostics().clear(); + self.previous_build_id().set(None); } /// Get the current stage. - pub fn stage(&self) -> BuildStage { - (self.stage)() + fn get_stage(&self) -> BuildStage { + self.stage().cloned() } /// Set the build stage. - pub fn set_stage(&mut self, stage: BuildStage) { - self.stage.set(stage); - } - - /// Get the current queue position. - pub fn queue_position(&self) -> Option { - (self.queue_position)() - } - - /// Set the queue position. - pub fn set_queue_position(&mut self, position: Option) { - self.queue_position.set(position); - } - - /// Get the diagnostics signal. - pub fn diagnostics(&self) -> Signal> { - self.diagnostics + fn set_stage(&mut self, stage: BuildStage) { + self.stage().set(stage); } /// Add another diagnostic to the current list. - pub fn push_diagnostic(&mut self, diagnostic: CargoDiagnostic) { - self.diagnostics.push(diagnostic); + fn push_diagnostic(&mut self, diagnostic: CargoDiagnostic) { + self.diagnostics().push(diagnostic); } } /// Start a build and handle updating the build signals according to socket messages. pub async fn start_build( - mut build: BuildState, + mut build: Store, socket_url: String, code: String, ) -> Result { + let stage = build.get_stage(); // Reset build state - if build.stage().is_running() { + if stage.is_running() { return Err(AppError::BuildIsAlreadyRunning); } build.reset(); + if let Some(build_id) = stage.finished_id() { + build.previous_build_id().set(Some(build_id)); + } build.set_stage(BuildStage::Starting); // Send socket compile request let mut socket = ws::Socket::new(&socket_url)?; - socket.send(SocketMessage::BuildRequest(code)).await?; + socket + .send(SocketMessage::BuildRequest { + code, + previous_build_id: stage.finished_id(), + }) + .await?; // Handle socket messages loop { @@ -133,8 +127,9 @@ pub async fn start_build( Err(e) => return Err(e), Ok(None) => break, Ok(Some(msg)) => { - let is_done = ws::handle_message(build, msg); - if is_done { + let finished = msg.is_finished(); + ws::handle_message(build, msg); + if finished { break; } } @@ -143,7 +138,7 @@ pub async fn start_build( socket.close().await; let mut success = false; - if let BuildStage::Finished(Ok(_)) = build.stage() { + if let BuildStage::Finished(Ok(_)) = build.get_stage() { success = true; }; diff --git a/packages/playground/playground/src/components/header.rs b/packages/playground/playground/src/components/header.rs index cb6d1e6848..741f078d87 100644 --- a/packages/playground/playground/src/components/header.rs +++ b/packages/playground/playground/src/components/header.rs @@ -1,12 +1,14 @@ -use crate::build::BuildState; +use crate::build::{BuildStage, BuildState, BuildStateStoreImplExt}; use crate::components::icons::LoadingSpinner; +use crate::dx_components::button::*; +use crate::dx_components::select::*; +use crate::hotreload::HotReloadStoreImplExt; use crate::share_code::copy_share_link; use crate::{Errors, PlaygroundUrls}; +use crate::{ErrorsStoreImplExt, HotReload}; use dioxus::prelude::*; -// use dioxus_sdk::utils::timing::use_debounce; use model::api::ApiClient; use model::Project; -use std::time::Duration; #[component] pub fn Header( @@ -15,50 +17,81 @@ pub fn Header( pane_left_width: Signal>, pane_right_width: Signal>, mut show_examples: Signal, - file_name: ReadOnlySignal, + file_name: ReadSignal, ) -> Element { - let build = use_context::(); - let api_client = use_context::>(); - let project = use_context::>(); - let mut errors = use_context::(); + let api_client: Signal = use_context(); + let mut build: Store = use_context(); + let mut project: Signal = use_context(); + let mut errors: Store = use_context(); + let mut hot_reload: Store = use_context(); let mut share_btn_text = use_signal(|| "Share"); - // let mut reset_share_btn = use_debounce(Duration::from_secs(1), move |()| { - // share_btn_text.set("Share") - // }); - // reset_share_btn.action(()); rsx! { div { id: "dxp-header", // Left pane header div { id: "dxp-header-left", - style: if let Some(val) = pane_left_width() { "width:{val}px;" }, // Examples button/menu - button { - id: "dxp-menu-btn", - class: "dxp-ctrl-btn", - class: if show_examples() { "dxp-open" }, - onclick: move |_| show_examples.toggle(), - crate::components::icons::MenuIcon {} + Select:: { + width: "75%", + value: Some(project()), + on_value_change: move |example: Option| { + use crate::monaco; + let Some(example) = example else { + return; + }; + + project.set(example.clone()); + build.set_stage(BuildStage::Finished(Ok(example.id()))); + monaco::set_current_model_value(&example.contents()); + hot_reload.set_starting_code(&example.contents()); + }, + SelectTrigger { + {file_name} + } + SelectList { + for (index, example) in example_projects::get_example_projects().iter().enumerate() { + SelectOption:: { + index, + value: example.clone(), + text_value: example.path.clone(), + div { + display: "flex", + flex_direction: "column", + align_items: "left", + padding: "0.25rem", + h3 { + margin: "0", + margin_bottom: ".25rem", + {example.path.clone()} + } + p { + margin: "0", + {example.description.clone()} + } + } + } + } + } } - button { class: "dxp-ctrl-btn dxp-file-btn dxp-selected-file", {file_name} } } // Right pane header div { id: "dxp-header-right", - style: if let Some(val) = pane_right_width() { "width:{val}px;" } else { "".to_string() }, // Share button - button { - id: "dxp-share-btn", - class: "dxp-ctrl-btn", + Button { + variant: ButtonVariant::Secondary, + id: "dxp-header-share-btn", onclick: move |_| async move { share_btn_text.set("Sharing..."); match copy_share_link(&api_client(), project, urls.location).await { - Ok(()) => share_btn_text.set("Link Copied!"), + Ok(()) => { + share_btn_text.set("Link Copied!"); + }, Err(error) => { share_btn_text.set("Error!"); errors @@ -76,25 +109,21 @@ pub fn Header( // Run button - button { - id: "dxp-run-btn", - class: "dxp-ctrl-btn", - class: if build.stage().is_running() { "disabled" }, + Button { + variant: ButtonVariant::Outline, + "data-disabled": build.get_stage().is_running(), + display: "flex", + flex_direction: "row", + align_items: "between", + justify_content: "center", + gap: "0.5rem", + width: "10rem", onclick: move |_| { on_rebuild.call(()); }, - if build.stage().is_running() { - LoadingSpinner {} - if let Some(pos) = build.queue_position() { - if pos == 0 { - "Building" - } else { - "#{pos}" - } - } else { - "Starting" - } + if build.get_stage().is_running() { + Progress {} } else { "Rebuild" } @@ -104,3 +133,33 @@ pub fn Header( } } } + +#[component] +fn Progress() -> Element { + let build = use_context::>(); + + // Generate the loading message. + let message = use_memo(move || match build.get_stage() { + BuildStage::NotStarted => "Waiting".to_string(), + BuildStage::Queued(position) => format!("Queued ({position})"), + BuildStage::Starting => "Starting".to_string(), + BuildStage::Waiting(time) => { + format!("Waiting {}s", time.as_secs()) + } + BuildStage::Building(build_stage) => match build_stage { + model::BuildStage::RunningBindgen => "Binding".to_string(), + model::BuildStage::Other => "Computing".to_string(), + model::BuildStage::Compiling { + crates_compiled, + total_crates, + .. + } => format!("{crates_compiled}/{total_crates}"), + }, + BuildStage::Finished(_) => "Finished!".to_string(), + }); + + rsx! { + LoadingSpinner {} + "{message}" + } +} diff --git a/packages/playground/playground/src/components/icons.rs b/packages/playground/playground/src/components/icons.rs index a5feaf5745..679eddb871 100644 --- a/packages/playground/playground/src/components/icons.rs +++ b/packages/playground/playground/src/components/icons.rs @@ -6,6 +6,7 @@ use dioxus::prelude::*; pub fn Warning() -> Element { rsx! { svg { + height: "16px", xmlns: "http://www.w3.org/2000/svg", fill: "#FFB11F", "viewBox": "0 -960 960 960", diff --git a/packages/playground/playground/src/components/logs.rs b/packages/playground/playground/src/components/logs.rs index 42a1c37558..1aba2f91b1 100644 --- a/packages/playground/playground/src/components/logs.rs +++ b/packages/playground/playground/src/components/logs.rs @@ -1,32 +1,50 @@ -use crate::build::BuildState; +use std::collections::HashMap; + +use crate::build::{BuildState, BuildStateStoreExt}; +use crate::dx_components::accordion::*; +use ansi_parser::AnsiParser; use dioxus::prelude::*; use model::{CargoDiagnosticSpan, CargoLevel}; #[component] pub fn Logs() -> Element { - let build = use_context::(); - let diagnostics = build.diagnostics()(); - let err_message = build.stage().err_message(); + let build = use_context::>(); + let diagnostics = build.diagnostics(); + let diagnostics = diagnostics.read(); + let diagnostics_with_spans = diagnostics.iter().filter(|d| !d.spans.is_empty()); + // Deduplicate diagnostics + let diagnostics_with_spans: HashMap<_, _> = diagnostics_with_spans + .enumerate() + .map(|(i, item)| (item, i)) + .collect(); + let mut diagnostics_with_spans: Vec<_> = diagnostics_with_spans.into_iter().collect(); + diagnostics_with_spans.sort_by_key(|(_, id)| *id); + let diagnostics_with_spans = diagnostics_with_spans + .into_iter() + .map(|(item, _)| item) + .cloned(); + let err_message = build.stage().read().err_message(); rsx! { - div { - id: "logs", - - // Main failure reason. - if let Some(message) = err_message { - Log { - level: CargoLevel::Error, - message, - spans: Vec::new(), - } + // Main failure reason. + if let Some(message) = err_message { + h2 { + "{message}" } + } + // Diagnostics + Accordion { + allow_multiple_open: true, + horizontal: false, // Log diagnostics - for diagnostic in diagnostics { + for (i, diagnostic) in diagnostics_with_spans.enumerate() { Log { + index: i, level: diagnostic.level, message: diagnostic.message, spans: diagnostic.spans, + rendered: diagnostic.rendered } } } @@ -34,51 +52,128 @@ pub fn Logs() -> Element { } #[component] -fn Log(level: CargoLevel, message: String, spans: Vec) -> Element { +fn Log( + index: usize, + level: CargoLevel, + message: String, + spans: Vec, + rendered: Option, +) -> Element { let level = match level { CargoLevel::Error => ("Error", "level-error"), CargoLevel::Warning => ("Warning", "level-warn"), }; rsx! { - div { - class: "log", - // Level - p { - class: "log-level", + AccordionItem { index, + AccordionTrigger { + display: "flex", + justify_content: "space-between", + align_items: "center", + padding: "0.5rem 1rem", + color: "black", + "{message}" span { - class: "{level.1}", "{level.0}" } } - - div { - class: "log-codeblock", - // Message - p { - class: "log-message", - "{message}", + AccordionContent { + RenderedLog { + rendered } + } + } + } +} + +#[component] +fn RenderedLog(rendered: Option) -> Element { + let Some(rendered) = rendered else { + return rsx! {}; + }; - for span in spans { - if let Some(label) = span.label { - p { - class: "log-span", - "-" - span { - class: "level-info", - " {span.line_start}" - } - ":" - span { - class: "level-info", - "{span.column_start}" - } - " {label}" - } - } + let mut fg_color = [0u8, 0, 0]; + let mut bg_color = [255u8, 255, 255]; + let mut bold = false; + let iter = rendered.ansi_parse().filter_map(|output| match output { + ansi_parser::Output::TextBlock(text) => { + let background_color = + format!("rgb({}, {}, {})", bg_color[0], bg_color[1], bg_color[2]); + let color = format!("rgb({}, {}, {})", fg_color[0], fg_color[1], fg_color[2]); + Some(rsx! { + span { + background_color, + color, + font_weight: if bold { 400 }, + {text} + } + }) + } + ansi_parser::Output::Escape(ansi_parser::AnsiSequence::SetGraphicsMode(mode)) => { + match mode.as_slice() { + [0] => { + fg_color = [0u8, 0, 0]; + bg_color = [255u8, 255, 255]; + bold = false; + } + [1] => { + bold = true; } + [38, 5, rgb_color] => fg_color = color_index_to_rgb(*rgb_color), + [48, 5, rgb_color] => bg_color = color_index_to_rgb(*rgb_color), + _ => {} } + None + } + other => { + tracing::info!("other: {other:?}"); + None + } + }); + + rsx! {pre { {iter} }} +} + +fn color_index_to_rgb(index: u8) -> [u8; 3] { + match index { + // Standard colors (0-15) + 0 => [0, 0, 0], // Black + 1 => [128, 0, 0], // Red + 2 => [0, 128, 0], // Green + 3 => [128, 128, 0], // Yellow + 4 => [0, 0, 128], // Blue + 5 => [128, 0, 128], // Magenta + 6 => [0, 128, 128], // Cyan + 7 => [192, 192, 192], // White + 8 => [128, 128, 128], // Bright Black (Gray) + 9 => [255, 0, 0], // Bright Red + 10 => [0, 255, 0], // Bright Green + 11 => [255, 255, 0], // Bright Yellow + 12 => [0, 0, 255], // Bright Blue + 13 => [255, 0, 255], // Bright Magenta + 14 => [0, 255, 255], // Bright Cyan + 15 => [255, 255, 255], // Bright White + + // 216-color cube (16-231) + 16..=231 => { + let cube_index = index - 16; + let r = cube_index / 36; + let g = (cube_index % 36) / 6; + let b = cube_index % 6; + + // Map 0-5 to RGB values + let value_map = [0, 95, 135, 175, 215, 255]; + [ + value_map[r as usize], + value_map[g as usize], + value_map[b as usize], + ] + } + + // Grayscale ramp (232-255) + 232..=255 => { + let gray = 8 + (index - 232) * 10; + [gray, gray, gray] } } } diff --git a/packages/playground/playground/src/components/modal.rs b/packages/playground/playground/src/components/modal.rs index a35f7fde63..58e5d62153 100644 --- a/packages/playground/playground/src/components/modal.rs +++ b/packages/playground/playground/src/components/modal.rs @@ -1,46 +1,54 @@ +use crate::dx_components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; use dioxus::prelude::*; +#[derive(Clone)] +struct ModalContext { + open: Signal, + on_ok: EventHandler, +} + +#[component] +pub fn Modal(on_ok: EventHandler, open: ReadSignal, children: Element) -> Element { + let mut internal_open = use_signal(|| false); + use_effect(move || internal_open.set(open())); + use_context_provider(move || ModalContext { + open: internal_open, + on_ok, + }); + rsx! { + DialogRoot { + open: internal_open(), + on_open_change: move |_| { + on_ok(()); + }, + {children} + } + } +} + #[component] -pub fn Modal( +pub fn ModalContent( icon: Element, title: String, text: String, ok_text: Option, - on_ok: EventHandler, ) -> Element { - let ok_text = ok_text.unwrap_or("Ok".to_string()); - + let ModalContext { open, on_ok } = use_context(); rsx! { - // Background - div { - id: "dxp-modal-bg", - - div { - id: "dxp-modal", - - // Modal header with optional icon - div { - id: "dxp-modal-header", - {icon} - h4 { - id: "dxp-modal-title", - "{title}" - } - } - - // Modal description text - p { - id: "dxp-modal-text", - "{text}" - } - - // ok button - button { - id: "dxp-modal-ok-btn", - onclick: move |_| on_ok.call(()), - "{ok_text}" - } + DialogContent { + button { + class: "dialog-close", + r#type: "button", + aria_label: "Close", + tabindex: if open() { "0" } else { "-1" }, + onclick: move |_| on_ok(()), + "×" + } + DialogTitle { + {icon} + "{title}" } + DialogDescription { "{text}" } } } } diff --git a/packages/playground/playground/src/components/panes.rs b/packages/playground/playground/src/components/panes.rs index bf287fca6c..3e00f8defa 100644 --- a/packages/playground/playground/src/components/panes.rs +++ b/packages/playground/playground/src/components/panes.rs @@ -1,7 +1,6 @@ -use crate::build::{BuildStage, BuildState}; +use crate::build::{BuildState, BuildStateStoreExt}; use dioxus::prelude::*; use dioxus_document::eval; -// use dioxus_sdk::utils::{timing::use_debounce, window::use_window_size}; use super::Logs; @@ -27,25 +26,10 @@ pub fn Panes( pane_right_width: Signal>, built_page_url: Memo>, ) -> Element { - let build = use_context::(); + let build = use_context::>(); let mut dragging = use_signal(|| false); let mut mouse_data = use_signal(DraggableData::default); - // // Reset the panes slider on window resize. - // // TODO: This is annoying for the user, it should instead just recalculate the size from previous data. - // let window_size = use_window_size(); - // let mut reset_panes_debounce = use_debounce(std::time::Duration::from_millis(200), move |_| { - // spawn(async move { - // pane_left_width.set(None); - // pane_right_width.set(None); - // }); - // }); - - // use_effect(move || { - // window_size(); - // reset_panes_debounce.action(()); - // }); - // Handle retrieving required data from dom elements and enabling drag. let draggable_mousedown = move |e: Event| async move { dragging.set(true); @@ -107,33 +91,43 @@ pub fn Panes( // Left Pane div { id: "dxp-panes-left", - style: if let Some(val) = pane_left_width() { "width:{val}px;" }, + width: if let Some(val) = pane_left_width() { "{val}px;" }, } // Draggable div { id: "dxp-panes-draggable", onmousedown: draggable_mousedown, onmouseup: stop_dragging, + // Two vertical lines to indicate draggable + svg { + width: "12", + height: "48", + xmlns: "http://www.w3.org/2000/svg", + view_box: "0 0 34 48", + fill: "none", + stroke: "currentColor", + stroke_width: "6", + stroke_linecap: "round", + stroke_linejoin: "round", + path { d: "M10 8v48" } + path { d: "M24 8v48" } + } } // Right Pane div { id: "dxp-panes-right", - style: if let Some(val) = pane_right_width() { "width:{val}px;" }, - - if build_stage.is_running() { - Progress {} - } else { - // Viewport - if let Some(url) = built_page_url() { - div { id: "dxp-viewport", - iframe { - id: "dxp-iframe", - src: "{url}", - pointer_events: if dragging() { "none" } else { "all" }, - } - } - } else if build_stage.is_err() { + width: if let Some(val) = pane_right_width() { "{val}px;" }, + + // Viewport + div { id: "dxp-viewport", + if build_stage().is_err() { Logs {} + } else if let Some(url) = built_page_url() { + iframe { + id: "dxp-iframe", + src: "{url}", + pointer_events: if dragging() { "none" } else { "all" }, + } } else { p { "Click `Rebuild` to start a build!" } } @@ -142,51 +136,3 @@ pub fn Panes( } } } - -#[component] -fn Progress() -> Element { - let build = use_context::(); - - // Generate the loading message. - let message = use_memo(move || { - let compiling = build.stage().get_compiling_stage(); - if let Some((crates_compiled, total_crates, current_crate)) = compiling { - return format!("[{crates_compiled}/{total_crates}] Compiling {current_crate}"); - } - - match build.stage() { - BuildStage::NotStarted => "Build has not started.", - BuildStage::Starting => "Starting build...", - BuildStage::Building(build_stage) => match build_stage { - model::BuildStage::RunningBindgen => "Running wasm-bindgen...", - model::BuildStage::Other => "Computing...", - model::BuildStage::Compiling { .. } => unreachable!(), - }, - BuildStage::Finished(_) => "Finished!", - } - .to_string() - }); - - // Determine the progress width. - let progress_width = use_memo(move || { - let stage = build.stage(); - let compiling = stage.get_compiling_stage(); - if let Some((crates_compiled, total_crates, _)) = compiling { - return (crates_compiled as f64 / total_crates as f64) * 100.0; - } - - match stage.is_running() { - true => 50.0, - false => 0.0, - } - }); - - rsx! { - div { id: "dxp-progress-container", - p { "{message}" } - div { id: "dxp-progress", - div { id: "dxp-bar", width: "{progress_width}%" } - } - } - } -} diff --git a/packages/playground/playground/src/debounce.rs b/packages/playground/playground/src/debounce.rs new file mode 100644 index 0000000000..e8b3b5590f --- /dev/null +++ b/packages/playground/playground/src/debounce.rs @@ -0,0 +1,315 @@ +// use crate::{use_timeout, TimeoutHandle, UseTimeout}; +use dioxus::{dioxus_core::SpawnIfAsync, hooks::use_signal, prelude::WritableExt, signals::Signal}; +use std::time::Duration; + +/// The interface for calling a debounce. +/// +/// See [`use_debounce`] for more information. +#[derive(Clone, Copy, PartialEq)] +pub struct UseDebounce { + current_handle: Signal>, + timeout: UseTimeout, +} + +impl UseDebounce { + /// Start the debounce countdown, resetting it if already started. + pub fn action(&mut self, args: Args) { + self.cancel(); + self.current_handle.set(Some(self.timeout.action(args))); + } + + /// Cancel the debounce action. + pub fn cancel(&mut self) { + if let Some(handle) = self.current_handle.take() { + handle.cancel(); + } + } +} + +/// A hook for allowing a function to be called only after a provided [`Duration`] has passed. +/// +/// Once the [`UseDebounce::action`] method is called, a timer will start counting down until +/// the callback is ran. If the [`UseDebounce::action`] method is called again, the timer will restart. +/// +/// # Examples +/// +/// Example of using a debounce: +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::use_debounce; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// // Create a two second debounce. +/// // This will print "ran" after two seconds since the last action call. +/// let mut debounce = use_debounce(Duration::from_secs(2), |_| println!("ran")); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// // Call the debounce. +/// debounce.action(()); +/// }, +/// "Click!" +/// } +/// } +/// } +/// ``` +/// +/// #### Cancelling A Debounce +/// If you need to cancel the currently active debounce, you can call [`UseDebounce::cancel`]: +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::use_debounce; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// let mut debounce = use_debounce(Duration::from_secs(5), |_| println!("ran")); +/// +/// rsx! { +/// button { +/// // Start the debounce on click. +/// onclick: move |_| debounce.action(()), +/// "Action!" +/// } +/// button { +/// // Cancel the debounce on click. +/// onclick: move |_| debounce.cancel(), +/// "Cancel!" +/// } +/// } +/// } +/// ``` +/// +/// ### Async Debounce +/// Debounces can accept an async callback: +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::use_debounce; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// // Create a two second debounce that uses some async/await. +/// let mut debounce = use_debounce(Duration::from_secs(2), |_| async { +/// println!("debounce called!"); +/// tokio::time::sleep(Duration::from_secs(2)).await; +/// println!("after async"); +/// }); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// // Call the debounce. +/// debounce.action(()); +/// }, +/// "Click!" +/// } +/// } +/// } +/// ``` +pub fn use_debounce, Marker>( + duration: Duration, + callback: impl FnMut(Args) -> MaybeAsync + 'static, +) -> UseDebounce { + let timeout = use_timeout(duration, callback); + let current_handle = use_signal(|| None); + + UseDebounce { + timeout, + current_handle, + } +} + +use dioxus::{ + core::Task, + // dioxus_core::SpawnIfAsync, + prelude::{spawn, use_hook, Callback}, + // signals::Signal, +}; +use futures::{channel::mpsc, SinkExt, StreamExt}; +// use std::time::Duration; + +/// The interface to a timeout. +/// +/// This is used to trigger the timeout with [`UseTimeout::action`]. +/// +/// See [`use_timeout`] for more information. +pub struct UseTimeout { + duration: Duration, + sender: Signal>, +} + +impl UseTimeout { + /// Trigger the timeout. + /// + /// If no arguments are desired, use the [`unit`] type. + /// See [`use_timeout`] for more information. + pub fn action(&self, args: Args) -> TimeoutHandle { + let mut sender = (self.sender)(); + let duration = self.duration; + + let handle = spawn(async move { + // #[cfg(not(target_family = "wasm"))] + // tokio::time::sleep(duration).await; + + #[cfg(target_family = "wasm")] + gloo_timers::future::sleep(duration).await; + + // If this errors then the timeout was likely dropped. + let _ = sender.send(args).await; + }); + + TimeoutHandle { handle } + } +} + +impl Clone for UseTimeout { + fn clone(&self) -> Self { + *self + } +} +impl Copy for UseTimeout {} +impl PartialEq for UseTimeout { + fn eq(&self, other: &Self) -> bool { + self.duration == other.duration && self.sender == other.sender + } +} + +/// A handle to a pending timeout. +/// +/// A handle to a running timeout triggered with [`UseTimeout::action`]. +/// This handle allows you to cancel the timeout from triggering with [`TimeoutHandle::cancel`] +/// +/// See [`use_timeout`] for more information. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TimeoutHandle { + handle: Task, +} + +impl TimeoutHandle { + /// Cancel the timeout associated with this handle. + pub fn cancel(self) { + self.handle.cancel(); + } +} + +/// A hook to run a callback after a period of time. +/// +/// Timeouts allow you to trigger a callback that occurs after a period of time. Unlike a debounce, a timeout will not +/// reset it's timer when triggered again. Instead, calling a timeout while it is already running will start another instance +/// to run the callback after the provided period. +/// +/// This hook is similar to the web [setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) API. +/// +/// # Examples +/// +/// Example of using a timeout: +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::use_timeout; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// // Create a timeout for two seconds. +/// // Once triggered, this timeout will print "timeout called" after two seconds. +/// let timeout = use_timeout(Duration::from_secs(2), |()| println!("timeout called")); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// // Trigger the timeout. +/// timeout.action(()); +/// }, +/// "Click!" +/// } +/// } +/// } +/// ``` +/// +/// #### Cancelling Timeouts +/// Example of cancelling a timeout. This is the equivalent of a debounce. +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::{use_timeout, TimeoutHandle}; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// let mut current_timeout: Signal> = use_signal(|| None); +/// let timeout = use_timeout(Duration::from_secs(2), move |()| { +/// current_timeout.set(None); +/// println!("timeout called"); +/// }); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// // Cancel any currently running timeouts. +/// if let Some(handle) = *current_timeout.read() { +/// handle.cancel(); +/// } +/// +/// // Trigger the timeout. +/// let handle = timeout.action(()); +/// current_timeout.set(Some(handle)); +/// }, +/// "Click!" +/// } +/// } +/// } +/// ``` +/// +/// #### Async Timeouts +/// Timeouts can accept an async callback: +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_time::use_timeout; +/// use std::time::Duration; +/// +/// #[component] +/// fn App() -> Element { +/// // Create a timeout for two seconds. +/// // We use an async sleep to wait an even longer duration after the timeout is called. +/// let timeout = use_timeout(Duration::from_secs(2), |()| async { +/// println!("Timeout after two total seconds."); +/// tokio::time::sleep(Duration::from_secs(2)).await; +/// println!("Timeout after four total seconds."); +/// }); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// // Trigger the timeout. +/// timeout.action(()); +/// }, +/// "Click!" +/// } +/// } +/// } +/// ``` +pub fn use_timeout, Marker>( + duration: Duration, + callback: impl FnMut(Args) -> MaybeAsync + 'static, +) -> UseTimeout { + use_hook(|| { + let callback = Callback::new(callback); + let (sender, mut receiver) = mpsc::unbounded(); + + spawn(async move { + loop { + if let Some(args) = receiver.next().await { + callback.call(args); + } + } + }); + + UseTimeout { + duration, + sender: Signal::new(sender), + } + }) +} diff --git a/packages/playground/playground/src/dx_components/accordion/component.rs b/packages/playground/playground/src/dx_components/accordion/component.rs new file mode 100644 index 0000000000..f9c44a92c0 --- /dev/null +++ b/packages/playground/playground/src/dx_components/accordion/component.rs @@ -0,0 +1,68 @@ +use dioxus::prelude::*; +use dioxus_primitives::accordion::{ + self, AccordionContentProps, AccordionItemProps, AccordionProps, AccordionTriggerProps, +}; + +#[component] +pub fn Accordion(props: AccordionProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + accordion::Accordion { + class: "accordion", + id: props.id, + allow_multiple_open: props.allow_multiple_open, + disabled: props.disabled, + collapsible: props.collapsible, + horizontal: props.horizontal, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn AccordionItem(props: AccordionItemProps) -> Element { + rsx! { + accordion::AccordionItem { + class: "accordion-item", + disabled: props.disabled, + default_open: props.default_open, + on_change: props.on_change, + on_trigger_click: props.on_trigger_click, + index: props.index, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn AccordionTrigger(props: AccordionTriggerProps) -> Element { + rsx! { + accordion::AccordionTrigger { + class: "accordion-trigger", + id: props.id, + attributes: props.attributes, + {props.children} + svg { + class: "accordion-expand-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "6 9 12 15 18 9" } + } + } + } +} + +#[component] +pub fn AccordionContent(props: AccordionContentProps) -> Element { + rsx! { + accordion::AccordionContent { + class: "accordion-content", + style: "--collapsible-content-width: 140px", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/packages/playground/playground/src/dx_components/accordion/mod.rs b/packages/playground/playground/src/dx_components/accordion/mod.rs new file mode 100644 index 0000000000..2590c01321 --- /dev/null +++ b/packages/playground/playground/src/dx_components/accordion/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/packages/playground/playground/src/dx_components/accordion/style.css b/packages/playground/playground/src/dx_components/accordion/style.css new file mode 100644 index 0000000000..7a21a4efeb --- /dev/null +++ b/packages/playground/playground/src/dx_components/accordion/style.css @@ -0,0 +1,95 @@ +.accordion-trigger { + display: flex; + width: 100%; + box-sizing: border-box; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0; + padding-top: 1rem; + padding-bottom: 1rem; + border: none; + background-color: transparent; + color: var(--secondary-color-4); + outline: none; + text-align: left; +} + +.accordion-trigger:focus-visible { + border: none; + box-shadow: inset 0 0 0 2px var(--focused-border-color); +} + +.accordion-trigger:hover { + cursor: pointer; + text-decoration-line: underline; +} + +.accordion-content { + display: grid; + height: 0; +} + +.accordion-content[data-open="false"] { + animation: accordion-slide-down 300ms cubic-bezier(0.87, 0, 0.13, 1) + forwards; +} + +.accordion-content[data-open="true"] { + animation: accordion-slide-up 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; +} + +@keyframes accordion-slide-down { + from { + height: var(--collapsible-content-width); + } + + to { + height: 0; + } +} + +@keyframes accordion-slide-up { + from { + height: 0; + } + + to { + height: var(--collapsible-content-width); + } +} + +.accordion-item { + overflow: hidden; + box-sizing: border-box; + border-bottom: 1px solid var(--primary-color-6); + margin-top: 1px; + width: 100%; +} + +.accordion-item:first-child { + margin-top: 0; +} + +.accordion-item:last-child { + border-bottom: none; +} + +.accordion-expand-icon { + width: 20px; + height: 20px; + fill: none; + stroke: black; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.accordion-item[data-open="true"] .accordion-expand-icon { + rotate: 180deg; +} + +.accordion { + width: 100%; +} diff --git a/packages/playground/playground/src/dx_components/button/component.rs b/packages/playground/playground/src/dx_components/button/component.rs new file mode 100644 index 0000000000..a7998a2da3 --- /dev/null +++ b/packages/playground/playground/src/dx_components/button/component.rs @@ -0,0 +1,59 @@ +use dioxus::prelude::*; + +#[derive(Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum ButtonVariant { + #[default] + Primary, + Secondary, + Outline, +} + +impl ButtonVariant { + pub fn class(&self) -> &'static str { + match self { + ButtonVariant::Primary => "primary", + ButtonVariant::Secondary => "secondary", + ButtonVariant::Outline => "outline", + } + } +} + +#[component] +pub fn Button( + #[props(default)] variant: ButtonVariant, + #[props(extends=GlobalAttributes)] + #[props(extends=button)] + attributes: Vec, + class: Option, + onclick: Option>, + onmousedown: Option>, + onmouseup: Option>, + children: Element, +) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + + button { + class: "button ".to_string() + class.as_deref().unwrap_or_default(), + "data-style": variant.class(), + onclick: move |event| { + if let Some(f) = &onclick { + f.call(event); + } + }, + onmousedown: move |event| { + if let Some(f) = &onmousedown { + f.call(event); + } + }, + onmouseup: move |event| { + if let Some(f) = &onmouseup { + f.call(event); + } + }, + ..attributes, + {children} + } + } +} diff --git a/packages/playground/playground/src/dx_components/button/mod.rs b/packages/playground/playground/src/dx_components/button/mod.rs new file mode 100644 index 0000000000..2590c01321 --- /dev/null +++ b/packages/playground/playground/src/dx_components/button/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/packages/playground/playground/src/dx_components/button/style.css b/packages/playground/playground/src/dx_components/button/style.css new file mode 100644 index 0000000000..9ab6176974 --- /dev/null +++ b/packages/playground/playground/src/dx_components/button/style.css @@ -0,0 +1,45 @@ +.button { + padding: 8px 18px; + border-radius: 0.5rem; + border: none; + cursor: pointer; + font-size: 1rem; + transition: + background-color 0.2s ease, + color 0.2s ease; +} +.button:focus-visible { + box-shadow: 0 0 0 2px var(--focused-border-color); +} + +.button[data-style="primary"] { + background-color: var(--secondary-color-2); + color: var(--primary-color); +} +.button[data-style="primary"]:hover { + background-color: var(--secondary-color-1); +} + +.button[data-style="secondary"] { + background-color: var(--primary-color-5); + color: var(--secondary-color-1); +} +.button[data-style="secondary"]:hover { + background-color: var(--primary-color-4); +} + +.button[data-style="outline"] { + border: 1px solid var(--primary-color-6); + background-color: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-3)); + color: var(--secondary-color-4); +} +.button[data-style="outline"]:hover { + background-color: var(--primary-color-4); +} + +.button[data-disabled="true"] { + background-color: var(--primary-color-6); + color: var(--secondary-color-5); + cursor: not-allowed; +} diff --git a/packages/playground/playground/src/dx_components/dialog/component.rs b/packages/playground/playground/src/dx_components/dialog/component.rs new file mode 100644 index 0000000000..63fd6097a4 --- /dev/null +++ b/packages/playground/playground/src/dx_components/dialog/component.rs @@ -0,0 +1,52 @@ +use dioxus::prelude::*; +use dioxus_primitives::dialog::{ + self, DialogContentProps, DialogDescriptionProps, DialogRootProps, DialogTitleProps, +}; + +#[component] +pub fn DialogRoot(props: DialogRootProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + dialog::DialogRoot { + class: "dialog-backdrop", + id: props.id, + is_modal: props.is_modal, + open: props.open, + default_open: props.default_open, + on_open_change: props.on_open_change, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn DialogContent(props: DialogContentProps) -> Element { + rsx! { + dialog::DialogContent { class: "dialog", id: props.id, attributes: props.attributes, {props.children} } + } +} + +#[component] +pub fn DialogTitle(props: DialogTitleProps) -> Element { + rsx! { + dialog::DialogTitle { + class: "dialog-title", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn DialogDescription(props: DialogDescriptionProps) -> Element { + rsx! { + dialog::DialogDescription { + class: "dialog-description", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/packages/playground/playground/src/dx_components/dialog/mod.rs b/packages/playground/playground/src/dx_components/dialog/mod.rs new file mode 100644 index 0000000000..2590c01321 --- /dev/null +++ b/packages/playground/playground/src/dx_components/dialog/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/packages/playground/playground/src/dx_components/dialog/style.css b/packages/playground/playground/src/dx_components/dialog/style.css new file mode 100644 index 0000000000..8884e0d532 --- /dev/null +++ b/packages/playground/playground/src/dx_components/dialog/style.css @@ -0,0 +1,104 @@ +/* Dialog Backdrop */ +.dialog-backdrop { + position: fixed; + z-index: 1000; + background: rgb(0 0 0 / 30%); + inset: 0; + opacity: 0; + will-change: transform, opacity; +} + +.dialog-backdrop[data-state="closed"] { + pointer-events: none; + animation: dialog-backdrop-animate-out 150ms ease-in forwards; +} + +@keyframes dialog-backdrop-animate-out { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + 100% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +.dialog-backdrop[data-state="open"] { + animation: dialog-content-animate-in 150ms ease-out forwards; +} + +@keyframes dialog-content-animate-in { + 0% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* Dialog Container - improved for theme consistency */ +.dialog { + position: fixed; + z-index: 1001; + top: 50%; + left: 50%; + display: flex; + width: 100%; + max-width: calc(100% - 2rem); + box-sizing: border-box; + flex-direction: column; + padding: 32px 24px 24px; + border: 1px solid var(--primary-color-6); + border-radius: 8px; + margin: 0; + background: var(--primary-color-2); + box-shadow: 0 2px 10px rgb(0 0 0 / 18%); + color: var(--secondary-color-4); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, + Arial, sans-serif; + gap: 16px; + text-align: center; + transform: translate(-50%, -50%); +} + +.dialog-title { + margin: 0; + color: var(--secondary-color-4); + font-size: 1.25rem; + font-weight: 700; +} + +.dialog-description { + margin: 0; + color: var(--secondary-color-5); + font-size: 1rem; +} + +@media (width >= 40rem) { + .dialog { + max-width: 32rem; + text-align: left; + } +} + +.dialog-close { + position: absolute; + top: 1rem; + right: 1rem; + align-self: flex-start; + padding: 0; + border: none; + margin: 0; + background: none; + color: var(--secondary-color-3); + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +.dialog-close:hover { + color: var(--secondary-color-1); +} diff --git a/packages/playground/playground/src/dx_components/mod.rs b/packages/playground/playground/src/dx_components/mod.rs new file mode 100644 index 0000000000..0bdf2f2e9a --- /dev/null +++ b/packages/playground/playground/src/dx_components/mod.rs @@ -0,0 +1,5 @@ +// AUTOGENERTED Components module +pub mod accordion; +pub mod button; +pub mod dialog; +pub mod select; diff --git a/packages/playground/playground/src/dx_components/select/component.rs b/packages/playground/playground/src/dx_components/select/component.rs new file mode 100644 index 0000000000..88e70f02d6 --- /dev/null +++ b/packages/playground/playground/src/dx_components/select/component.rs @@ -0,0 +1,116 @@ +use dioxus::prelude::*; +use dioxus_primitives::select::{ + self, SelectGroupLabelProps, SelectGroupProps, SelectListProps, SelectOptionProps, SelectProps, + SelectTriggerProps, SelectValueProps, +}; + +#[component] +pub fn Select(props: SelectProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + select::Select { + class: "select", + value: props.value, + default_value: props.default_value, + on_value_change: props.on_value_change, + disabled: props.disabled, + name: props.name, + placeholder: props.placeholder, + roving_loop: props.roving_loop, + typeahead_timeout: props.typeahead_timeout, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectTrigger(props: SelectTriggerProps) -> Element { + rsx! { + select::SelectTrigger { class: "select-trigger", attributes: props.attributes, + {props.children} + svg { + class: "select-expand-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "6 9 12 15 18 9" } + } + } + } +} + +#[component] +pub fn SelectValue(props: SelectValueProps) -> Element { + rsx! { + select::SelectValue { attributes: props.attributes } + } +} + +#[component] +pub fn SelectList(props: SelectListProps) -> Element { + rsx! { + select::SelectList { + class: "select-list", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroup(props: SelectGroupProps) -> Element { + rsx! { + select::SelectGroup { + class: "select-group", + disabled: props.disabled, + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element { + rsx! { + select::SelectGroupLabel { + class: "select-group-label", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectOption(props: SelectOptionProps) -> Element { + rsx! { + select::SelectOption:: { + class: "select-option", + value: props.value, + text_value: props.text_value, + disabled: props.disabled, + id: props.id, + index: props.index, + aria_label: props.aria_label, + aria_roledescription: props.aria_roledescription, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectItemIndicator() -> Element { + rsx! { + select::SelectItemIndicator { + svg { + class: "select-check-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { d: "M5 13l4 4L19 7" } + } + } + } +} diff --git a/packages/playground/playground/src/dx_components/select/mod.rs b/packages/playground/playground/src/dx_components/select/mod.rs new file mode 100644 index 0000000000..2590c01321 --- /dev/null +++ b/packages/playground/playground/src/dx_components/select/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/packages/playground/playground/src/dx_components/select/style.css b/packages/playground/playground/src/dx_components/select/style.css new file mode 100644 index 0000000000..31f0eab441 --- /dev/null +++ b/packages/playground/playground/src/dx_components/select/style.css @@ -0,0 +1,153 @@ +.select { + position: relative; +} + +.select-trigger { + position: relative; + display: flex; + box-sizing: border-box; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0.25rem; + padding: 8px 12px; + border: none; + border-radius: 0.5rem; + border-radius: calc(0.5rem); + background: none; + background: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-3)); + box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + color: var(--secondary-color-4); + cursor: pointer; + gap: 0.25rem; + transition: background-color 100ms ease-out; +} + +.select-trigger span[data-placeholder="true"] { + color: var(--secondary-color-5); +} + +.select[data-state="open"] .select-trigger { + pointer-events: none; +} + +.select-expand-icon { + width: 20px; + height: 20px; + fill: none; + stroke: var(--primary-color-7); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select-check-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: var(--secondary-color-5); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select[data-disabled="true"] .select-trigger { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-trigger:hover:not([data-disabled="true"]), +.select-trigger:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-5)); + color: var(--secondary-color-1); + outline: none; +} + +.select-list { + position: absolute; + z-index: 1000; + top: 100%; + left: 0; + min-width: 100%; + box-sizing: border-box; + padding: 0.25rem; + border-radius: 0.5rem; + margin-top: 0.25rem; + background: var(--light, var(--primary-color)) + var(--dark, var(--primary-color-5)); + box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + transform-origin: top; + opacity: 0; + pointer-events: none; + will-change: transform, opacity; +} + +.select-list[data-state="closed"] { + pointer-events: none; + animation: select-list-animate-out 150ms ease-in forwards; +} + +@keyframes select-list-animate-out { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + 100% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +.select-list[data-state="open"] { + pointer-events: auto; + animation: select-list-animate-in 150ms ease-out forwards; +} + +@keyframes select-list-animate-in { + 0% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.select-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: calc(0.5rem - 0.25rem); + cursor: pointer; + font-size: 14px; +} + +.select-option[data-disabled="true"] { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-option:hover:not([data-disabled="true"]), +.select-option:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-7)); + color: var(--secondary-color-1); + outline: none; +} + +.select-group-label { + padding: 4px 12px; + color: var(--secondary-color-5); + font-size: 0.75rem; +} + +[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/packages/playground/playground/src/editor/monaco.js b/packages/playground/playground/src/editor/monaco.js index 56cb26e077..e5c47fd051 100644 --- a/packages/playground/playground/src/editor/monaco.js +++ b/packages/playground/playground/src/editor/monaco.js @@ -5,8 +5,9 @@ export function initMonaco( vsPathPrefix, elementId, initialTheme, - initialSnippet, + initialSnippet, onReadyCallback, + onBuildCallback, ) { require.config({ paths: { vs: vsPathPrefix } }); @@ -87,6 +88,11 @@ export function initMonaco( "semanticHighlighting.enabled": true, }); + // Build on Ctrl+Enter / Cmd+Enter + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { + onBuildCallback(); + }); + monacoEditor = editor; currentMonacoModel = model; }); diff --git a/packages/playground/playground/src/editor/monaco.rs b/packages/playground/playground/src/editor/monaco.rs index 4d413c9ce5..0fee297397 100644 --- a/packages/playground/playground/src/editor/monaco.rs +++ b/packages/playground/playground/src/editor/monaco.rs @@ -1,13 +1,12 @@ use crate::hotreload::HotReload; +use crate::hotreload::HotReloadStoreImplExt; +use crate::theme::Theme; use dioxus::prelude::*; -// use dioxus_sdk::utils::timing::UseDebounce; +// use dioxus_sdk::window::theme::Theme; use model::{CargoDiagnostic, CargoLevel}; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; -// #[cfg(target_arch = "wasm32")] -// use dioxus_sdk::theme::SystemTheme; - /// Get the path prefix for the `/vs` folder inside the Monaco folder. pub fn monaco_vs_prefix(folder: Asset) -> String { let monaco_vs_prefix = format!("{}/vs", folder); @@ -21,7 +20,7 @@ pub fn monaco_loader_src(folder: Asset) -> String { } /// Use monaco code markers for build diagnostics. -pub fn set_monaco_markers(diagnostics: Signal>) { +pub fn set_monaco_markers(diagnostics: impl Writable>) { let mut markers = Vec::new(); for diagnostic in diagnostics.read().iter() { let severity = match diagnostic.level { @@ -52,35 +51,37 @@ pub fn set_monaco_markers(diagnostics: Signal>) { set_markers(&markers); } -// /// Initialize Monaco once the loader script loads. -// #[cfg(target_arch = "wasm32")] -// pub fn on_monaco_load( -// folder: Asset, -// system_theme: SystemTheme, -// contents: &str, -// mut hot_reload: HotReload, -// mut monaco_ready: Signal, -// mut on_model_changed: UseDebounce, -// ) { -// let on_ready_callback = Closure::new(move || monaco_ready.set(true)); -// let monaco_prefix = monaco_vs_prefix(folder); -// init( -// &monaco_prefix, -// super::EDITOR_ELEMENT_ID, -// system_theme, -// contents, -// &on_ready_callback, -// ); - -// hot_reload.set_starting_code(contents); - -// let model_change_callback = -// Closure::new(move |new_code: String| on_model_changed.action(new_code)); -// register_model_change_event(&model_change_callback); - -// on_ready_callback.forget(); -// model_change_callback.forget(); -// } +/// Initialize Monaco once the loader script loads. +pub fn on_monaco_load( + folder: Asset, + system_theme: Theme, + contents: &str, + mut hot_reload: Store, + mut monaco_ready: Signal, + on_model_changed: Callback, + onbuild_callback: Callback<()>, +) { + let on_ready_callback = Closure::new(move || monaco_ready.set(true)); + let monaco_prefix = monaco_vs_prefix(folder); + let onbuild_callback = Closure::new(move || onbuild_callback.call(())); + init( + &monaco_prefix, + super::EDITOR_ELEMENT_ID, + system_theme, + contents, + &on_ready_callback, + &onbuild_callback, + ); + + hot_reload.set_starting_code(contents); + + let model_change_callback = Closure::new(move |new_code: String| on_model_changed(new_code)); + register_model_change_event(&model_change_callback); + + on_ready_callback.forget(); + model_change_callback.forget(); + onbuild_callback.forget(); +} #[derive(Serialize, Deserialize, Clone)] pub struct Marker { @@ -123,6 +124,7 @@ extern "C" { initial_theme: &str, initial_snippet: &str, on_ready_callback: &Closure, + on_build_callback: &Closure, ); #[wasm_bindgen(js_name = getCurrentModelValue)] @@ -147,13 +149,13 @@ extern "C" { fn register_model_change_event(callback: &Closure); } -#[cfg(target_arch = "wasm32")] pub fn init( vs_path_prefix: &str, element_id: &str, - initial_theme: SystemTheme, + initial_theme: Theme, initial_snippet: &str, on_ready_callback: &Closure, + on_build_callback: &Closure, ) { let theme = system_theme_to_string(initial_theme); init_monaco( @@ -162,12 +164,12 @@ pub fn init( &theme, initial_snippet, on_ready_callback, + on_build_callback, ); register_paste_as_rsx_action(); } -#[cfg(target_arch = "wasm32")] -pub fn set_theme(theme: SystemTheme) { +pub fn set_theme(theme: Theme) { let theme = system_theme_to_string(theme); set_monaco_theme(&theme); } @@ -183,11 +185,10 @@ fn register_paste_as_rsx_action() { callback.forget(); } -#[cfg(target_arch = "wasm32")] -fn system_theme_to_string(theme: SystemTheme) -> String { +fn system_theme_to_string(theme: Theme) -> String { match theme { - SystemTheme::Light => "dx-vs", - SystemTheme::Dark => "dx-vs-dark", + Theme::Light => "dx-vs", + Theme::Dark => "dx-vs-dark", } .to_string() } diff --git a/packages/playground/playground/src/hotreload.rs b/packages/playground/playground/src/hotreload.rs index f926a10283..ffc51c589e 100644 --- a/packages/playground/playground/src/hotreload.rs +++ b/packages/playground/playground/src/hotreload.rs @@ -1,9 +1,9 @@ //! Simplified hot reloading for a single main.rs file. +use ::dioxus_devtools::HotReloadMsg; use dioxus::{logger::tracing::error, prelude::*}; use dioxus_core::internal::{ HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey, }; -use dioxus_devtools::HotReloadMsg; use dioxus_document::eval; use dioxus_html::HtmlCtx; use dioxus_rsx::CallBody; @@ -12,7 +12,7 @@ use std::{collections::HashMap, fmt::Display, path::Path}; use syn::spanned::Spanned as _; /// Atempts to hot reload and returns true if a full rebuild is needed. -pub fn attempt_hot_reload(mut hot_reload: HotReload, new_code: &str) { +pub fn attempt_hot_reload(mut hot_reload: Store, new_code: &str) { // Process any potential hot -eloadable changes and send them to the iframe web client. let result = hot_reload.process_file_change(new_code.to_string()); match result { @@ -24,21 +24,8 @@ pub fn attempt_hot_reload(mut hot_reload: HotReload, new_code: &str) { jump_table: Default::default(), for_build_id: Default::default(), for_pid: Default::default(), - // unknown_files: Vec::new(), }; - - let e = eval( - r#" - const hrMsg = await dioxus.recv(); - const iframeElem = document.getElementById("dxp-iframe"); - const hrMsgJson = JSON.stringify(hrMsg); - - if (iframeElem) { - iframeElem.contentWindow.postMessage(hrMsgJson, "*"); - } - "#, - ); - _ = e.send(hr_msg); + send_hot_reload(hr_msg); } Err(HotReloadError::NeedsRebuild) => hot_reload.set_needs_rebuild(true), Err(e) => { @@ -48,12 +35,28 @@ pub fn attempt_hot_reload(mut hot_reload: HotReload, new_code: &str) { } } -#[derive(Clone, Copy)] +pub fn send_hot_reload(hr_msg: HotReloadMsg) { + let e = eval( + r#" + const hrMsg = await dioxus.recv(); + const iframeElem = document.getElementById("dxp-iframe"); + const hrMsgJson = JSON.stringify(hrMsg); + + if (iframeElem) { + iframeElem.contentWindow.postMessage(hrMsgJson, "*"); + } + "#, + ); + _ = e.send(hr_msg); +} + +#[derive(Store)] pub struct HotReload { - needs_rebuild: Signal, - cached_parse: Signal, + needs_rebuild: bool, + cached_parse: CachedParse, } +#[derive(Store)] struct CachedParse { raw: String, templates: HashMap, @@ -63,48 +66,44 @@ impl HotReload { pub fn new() -> Self { Self { cached_parse: { - Signal::new(CachedParse { + CachedParse { raw: String::new(), templates: HashMap::new(), - }) + } }, - needs_rebuild: Signal::new(true), + needs_rebuild: true, } } +} - pub fn set_needs_rebuild(&mut self, needs_rebuild: bool) { - self.needs_rebuild.set(needs_rebuild); +#[store(pub)] +impl Store { + fn set_needs_rebuild(&mut self, needs_rebuild: bool) { + self.needs_rebuild().set(needs_rebuild); } - pub fn set_starting_code(&mut self, code: &str) { - *self.cached_parse.write() = CachedParse { + fn set_starting_code(&mut self, code: &str) { + *self.cached_parse().write() = CachedParse { raw: code.to_string(), templates: HashMap::new(), }; } - fn full_rebuild(&mut self, code: String) -> HotReloadError { - *self.cached_parse.write() = CachedParse { - raw: code, - templates: HashMap::new(), - }; - HotReloadError::NeedsRebuild - } - - pub fn process_file_change( + fn process_file_change( &mut self, new_code: String, ) -> Result, HotReloadError> { let new_file = syn::parse_file(&new_code).map_err(|_err| HotReloadError::Parse)?; let cached_file = { - let cached = &mut self.cached_parse.read(); + let cached = self.cached_parse(); + let cached = cached.read(); syn::parse_file(&cached.raw).map_err(|_err| HotReloadError::Parse)? }; let changes = match diff_rsx(&new_file, &cached_file) { Some(rsx_calls) => rsx_calls, - None => return Err(self.full_rebuild(new_code)), + None => return Err(HotReloadError::NeedsRebuild), }; let mut out_templates = Vec::new(); @@ -129,10 +128,11 @@ impl HotReload { // if the template is not hotreloadable, we need to do a full rebuild let Some(results) = hotreload_result else { - return Err(self.full_rebuild(new_code)); + return Err(HotReloadError::NeedsRebuild); }; - let mut cached = self.cached_parse.write(); + let mut cached = self.cached_parse(); + let mut cached = cached.write(); for (index, template) in results.templates { if template.roots.is_empty() { continue; diff --git a/packages/playground/playground/src/lib.rs b/packages/playground/playground/src/lib.rs index d4b8492543..1cc8b7570c 100644 --- a/packages/playground/playground/src/lib.rs +++ b/packages/playground/playground/src/lib.rs @@ -1,281 +1,295 @@ -// use build::{start_build, BuildStage, BuildState}; -// use components::icons::Warning; -// use dioxus::logger::tracing::error; -// use dioxus::prelude::*; -// use dioxus_document::Link; -// // use dioxus_sdk::utils::timing::use_debounce; -// use editor::monaco::{self, monaco_loader_src, set_monaco_markers}; -// use hotreload::{attempt_hot_reload, HotReload}; -// use model::{api::ApiClient, AppError, Project, SocketError}; -// use std::time::Duration; - -// // #[cfg(target_arch = "wasm32")] -// // use dioxus_sdk::theme::{use_system_theme, SystemTheme}; - -// mod build; -// mod components; -// mod editor; -// mod hotreload; -// mod share_code; -// mod ws; - -// const DXP_CSS: Asset = asset!("/assets/dxp.css"); -// const MONACO_FOLDER: Asset = asset!("/assets/monaco-editor-0.52.2"); - -// /// The URLS that the playground should use for locating resources and services. -// #[derive(Debug, Clone, PartialEq)] -// pub struct PlaygroundUrls { -// /// The URL to the websocket server. -// pub socket: &'static str, -// /// The URL to the built project files from the server. -// pub server: &'static str, -// /// The url location of the playground UI: e.g. `https://dioxuslabs.com/play` -// pub location: &'static str, -// } - -// #[component] -// pub fn Playground( -// urls: PlaygroundUrls, -// share_code: ReadOnlySignal>, -// class: Option, -// ) -> Element { -// let mut build = use_context_provider(BuildState::new); -// let mut hot_reload = use_context_provider(HotReload::new); -// let api_client = use_context_provider(|| Signal::new(ApiClient::new(urls.server))); -// let mut errors = use_context_provider(Errors::new); - -// let monaco_ready = use_signal(|| false); -// let mut show_share_warning = use_signal(|| false); - -// // Default to the welcome project. -// // Project dirty determines whether the Rust-project is synced with the project in the editor. -// let mut project = use_context_provider(|| Signal::new(example_projects::get_welcome_project())); -// let mut project_dirty = use_signal(|| false); -// use_effect(move || { -// if project_dirty() && monaco_ready() { -// let project = project.read(); -// monaco::set_current_model_value(&project.contents()); -// project_dirty.set(false); -// } -// }); - -// // Get the shared project if a share code was provided. -// use_effect(move || { -// if let Some(share_code) = share_code() { -// spawn(async move { -// let api_client = api_client(); -// let shared_project = Project::from_share_code(&api_client, share_code).await; -// if let Ok(shared_project) = shared_project { -// show_share_warning.set(true); -// project_dirty.set(true); -// project.set(shared_project); -// } -// }); -// } -// }); - -// // // Handle events when code changes. -// // let on_model_changed = use_debounce(Duration::from_millis(250), move |new_code: String| { -// // // Update the project -// // project.write().set_contents(new_code.clone()); -// // spawn(async move { -// // editor::monaco::set_markers(&[]); - -// // if build.stage().is_finished() { -// // attempt_hot_reload(hot_reload, &new_code); -// // } -// // }); -// // }); - -// // Handle setting diagnostics based on build state. -// use_effect(move || set_monaco_markers(build.diagnostics())); - -// // Themes -// #[cfg(target_arch = "wasm32")] -// let system_theme = use_system_theme(); -// use_effect(move || { -// #[cfg(target_arch = "wasm32")] -// editor::monaco::set_theme(system_theme().unwrap_or(SystemTheme::Light)); -// }); - -// // Handle starting a build. -// let on_rebuild = move |_| async move { -// if build.stage().is_running() || !monaco_ready() { -// return; -// } -// hot_reload.set_needs_rebuild(false); - -// // Update hot reload -// let code = editor::monaco::get_current_model_value(); - -// let socket_url = urls.socket.to_string(); -// match start_build(build, socket_url, code).await { -// Ok(success) => hot_reload.set_needs_rebuild(!success), -// Err(error) => errors.push_from_app_error(error), -// } -// }; - -// // Construct the full URL to the built project. -// let built_page_url = use_memo(move || { -// let prebuilt_id = project.read().prebuilt.then_some(project.read().id()); -// let local_id = build.stage().finished_id(); -// let id = local_id.or(prebuilt_id)?; -// Some(format!("{}/built/{}", urls.server, id)) -// }); - -// // State for pane resizing, shared by headers and panes. -// // The actual logic is in the panes component. -// let mut pane_left_width: Signal> = use_signal(|| None); -// let mut pane_right_width: Signal> = use_signal(|| None); - -// // Show the example list -// let show_examples = use_signal(|| true); -// use_effect(move || { -// let _show_examples = show_examples(); -// pane_left_width.set(None); -// pane_right_width.set(None); -// }); - -// rsx! { -// div { class, id: "dxp-playground-root", -// // Head elements -// Link { rel: "stylesheet", href: DXP_CSS } - -// // Monaco script -// script { -// src: monaco_loader_src(MONACO_FOLDER), -// onload: move |_| { -// #[cfg(target_arch = "wasm32")] -// monaco::on_monaco_load( -// MONACO_FOLDER, -// system_theme().unwrap_or(SystemTheme::Light), -// &project.read().contents(), -// hot_reload, -// monaco_ready, -// on_model_changed, -// ); -// }, -// } - -// // Share warning -// if show_share_warning() { -// components::Modal { -// icon: rsx! { -// Warning {} -// }, -// title: "Do you trust this code?", -// text: "Anyone can share their project. Verify that nothing malicious has been included before running this project.", -// ok_text: "I understand", -// on_ok: move |_| show_share_warning.set(false), -// } -// } - -// // Show errors one at a time. -// if let Some(error) = errors.first() { -// components::Modal { -// icon: rsx! { -// Warning {} -// }, -// title: "{error.0}", -// text: "{error.1}", -// on_ok: move |_| { -// errors.pop(); -// }, -// } -// } - -// // Playground UI -// components::Header { -// urls, -// on_rebuild, -// show_examples, -// pane_left_width, -// pane_right_width, -// file_name: project.read().path.clone(), -// } -// div { id: "dxp-lower-half", -// div { -// id: "dxp-examples-list", -// class: if show_examples() { "dxp-open" } else { "" }, -// for example in example_projects::get_example_projects().iter() { -// button { -// class: "dxp-example-project", -// onclick: move |_| { -// project.set(example.clone()); -// build.set_stage(BuildStage::Finished(Ok(example.id()))); -// monaco::set_current_model_value(&example.contents()); -// hot_reload.set_starting_code(&example.contents()); -// }, -// h3 { {example.path.clone()} } -// p { {example.description.clone()} } -// } -// } -// } -// components::Panes { -// pane_left_width, -// pane_right_width, -// built_page_url, -// } -// } - -// } -// } -// } - -// /// A helper type for gracefully handling app errors and logging them. -// #[derive(Clone, Copy)] -// pub struct Errors { -// errors: Signal>, -// } - -// impl Errors { -// pub fn new() -> Self { -// Self { -// errors: Signal::new(Vec::new()), -// } -// } - -// pub fn push_error(&mut self, error: (impl ToString, impl ToString)) { -// let error = (error.0.to_string(), error.1.to_string()); -// error!(?error, "an error occured and was handled gracefully"); -// self.errors.push(error); -// } - -// pub fn push_from_app_error(&mut self, app_error: AppError) { -// let error = match app_error { -// AppError::Parse(error) => ("Parse Error", error.to_string()), -// AppError::Request(error) => ("Request Error", error.to_string()), -// AppError::ResourceNotFound => ( -// "Resource Not Found", -// "A requested resource was not found.".to_string(), -// ), -// AppError::Socket(error) => ( -// "Socket Error", -// match error { -// SocketError::ParseJson(error) => error.to_string(), -// SocketError::Utf8Decode(_) => "UTF-8 decode failed".to_string(), -// SocketError::Gloo(web_socket_error) => web_socket_error.to_string(), -// e => e.to_string(), -// }, -// ), -// AppError::Js(error) => ("JS Error", error.to_string()), -// _ => return, -// }; - -// self.push_error(error); -// } - -// pub fn first(&self) -> Option<(String, String)> { -// self.errors.first().map(|x| x.clone()) -// } - -// pub fn pop(&mut self) -> Option<(String, String)> { -// self.errors.pop() -// } -// } - -// impl Default for Errors { -// fn default() -> Self { -// Self::new() -// } -// } +use build::{start_build, BuildState}; +use components::icons::Warning; +use dioxus::logger::tracing::error; +use dioxus::prelude::*; +use dioxus_document::Link; +// use dioxus_sdk::time::use_debounce; +use editor::monaco::{self, monaco_loader_src, set_monaco_markers}; +use hotreload::{attempt_hot_reload, HotReload}; +use model::{api::ApiClient, AppError, Project, SocketError}; +use std::time::Duration; + +mod theme; +use theme::{get_theme, use_system_theme, Theme}; +mod debounce; +use debounce::use_debounce; + +// use dioxus_sdk::window::theme::{use_system_theme, Theme}; + +use crate::{ + build::{BuildStateStoreExt, BuildStateStoreImplExt}, + hotreload::HotReloadStoreImplExt, +}; + +mod build; +mod components; +mod dx_components; +mod editor; +mod hotreload; +mod share_code; +mod ws; + +const DXP_CSS: Asset = asset!("/assets/dxp.css"); +const MONACO_FOLDER: Asset = asset!("/assets/monaco-editor-0.52.2"); +const DX_COMPONENTS_CSS: Asset = asset!("/assets/dx-components-theme.css"); + +/// The URLS that the playground should use for locating resources and services. +#[derive(Debug, Clone, PartialEq)] +pub struct PlaygroundUrls { + /// The URL to the websocket server. + pub socket: &'static str, + /// The URL to the built project files from the server. + pub server: &'static str, + /// The url location of the playground UI: e.g. `https://dioxuslabs.com/play` + pub location: &'static str, +} + +#[component] +pub fn Playground( + urls: PlaygroundUrls, + share_code: ReadSignal>, + class: Option, +) -> Element { + let mut hot_reload = use_context_provider(|| Store::new(HotReload::new())); + let api_client = use_context_provider(|| Signal::new(ApiClient::new(urls.server))); + let mut errors = use_context_provider(|| Store::new(Errors::new())); + + let monaco_ready = use_signal(|| false); + let mut show_share_warning = use_signal(|| false); + + // Default to the welcome project. + // Project dirty determines whether the Rust-project is synced with the project in the editor. + let mut project = use_context_provider(|| Signal::new(example_projects::get_welcome_project())); + let mut build = use_context_provider(|| Store::new(BuildState::new(&project.read()))); + let mut project_dirty = use_signal(|| false); + use_effect(move || { + if project_dirty() && monaco_ready() { + let project = project.read(); + monaco::set_current_model_value(&project.contents()); + project_dirty.set(false); + } + }); + + // Get the shared project if a share code was provided. + use_effect(move || { + if let Some(share_code) = share_code() { + spawn(async move { + let api_client = api_client(); + let shared_project = Project::from_share_code(&api_client, share_code).await; + if let Ok(shared_project) = shared_project { + show_share_warning.set(true); + project_dirty.set(true); + project.set(shared_project); + build.reset(); + } + }); + } + }); + + // Handle events when code changes. + let mut on_model_changed = use_debounce(Duration::from_millis(250), move |new_code: String| { + // Update the project + project.write().set_contents(new_code.clone()); + spawn(async move { + editor::monaco::set_markers(&[]); + + if build.get_stage().is_finished() { + attempt_hot_reload(hot_reload, &new_code); + } + }); + }); + + let on_model_changed = use_callback(move |args| on_model_changed.action(args)); + + // Handle setting diagnostics based on build state. + use_effect(move || set_monaco_markers(build.diagnostics())); + + // // Themes + // let system_theme = use_system_theme(); + // use_effect(move || { + // editor::monaco::set_theme(system_theme().unwrap_or(Theme::Light)); + // }); + + // Handle starting a build. + let on_rebuild = use_callback(move |_| { + spawn(async move { + if build.get_stage().is_running() || !monaco_ready() { + return; + } + + // Update hot reload + let code = editor::monaco::get_current_model_value(); + + let socket_url = urls.socket.to_string(); + match start_build(build, socket_url, code.clone()).await { + Ok(success) => { + if success { + hot_reload.set_starting_code(&code); + } + hot_reload.set_needs_rebuild(!success) + } + Err(error) => errors.push_from_app_error(error), + } + }); + }); + + // Construct the full URL to the built project. + let built_page_url = use_memo(move || { + let project = project.read(); + let prebuilt_id = project.prebuilt.then_some(project.id()); + let local_id = build.get_stage().finished_id(); + let id = local_id.or(prebuilt_id)?; + Some(format!("{}/built/{}", urls.server, id)) + }); + + // State for pane resizing, shared by headers and panes. + // The actual logic is in the panes component. + let mut pane_left_width: Signal> = use_signal(|| None); + let mut pane_right_width: Signal> = use_signal(|| None); + + // Show the example list + let show_examples = use_signal(|| true); + use_effect(move || { + let _show_examples = show_examples(); + pane_left_width.set(None); + pane_right_width.set(None); + }); + + rsx! { + div { class, id: "dxp-playground-root", + // Head elements + Link { rel: "stylesheet", href: DXP_CSS } + Link { rel: "stylesheet", href: DX_COMPONENTS_CSS } + + // Monaco script + script { + src: monaco_loader_src(MONACO_FOLDER), + onload: move |_| { + monaco::on_monaco_load( + MONACO_FOLDER, + get_theme().unwrap_or(Theme::Light), + &project.read().contents(), + hot_reload, + monaco_ready, + on_model_changed, + on_rebuild + ); + }, + } + + // Share warning + components::Modal { + on_ok: move |_| show_share_warning.set(false), + open: show_share_warning(), + if show_share_warning() { + components::ModalContent { + icon: rsx! { + Warning {} + }, + title: "Do you trust this code?", + text: "Anyone can share their project. Verify that nothing malicious has been included before running this project.", + ok_text: "I understand", + } + } + } + + // Show errors one at a time. + components::Modal { + on_ok: move |_| { + errors.pop(); + }, + open: !errors.errors().is_empty(), + if let Some((title, text)) = errors.last() { + components::ModalContent { + icon: rsx! { + Warning {} + }, + title, + text, + } + }, + } + + // Playground UI + components::Header { + urls, + on_rebuild, + show_examples, + pane_left_width, + pane_right_width, + file_name: project.read().path.clone(), + } + div { id: "dxp-lower-half", + components::Panes { + pane_left_width, + pane_right_width, + built_page_url, + } + } + + } + } +} + +/// A helper type for gracefully handling app errors and logging them. +#[derive(Clone, Store)] +pub struct Errors { + errors: Vec<(String, String)>, +} + +impl Errors { + pub fn new() -> Self { + Self { errors: Vec::new() } + } +} + +#[store(pub)] +impl Store { + fn push_error(&mut self, error: (impl ToString, impl ToString)) { + let error = (error.0.to_string(), error.1.to_string()); + error!(?error, "an error occured and was handled gracefully"); + self.errors().push(error); + } + + fn push_from_app_error(&mut self, app_error: AppError) { + let error = match app_error { + AppError::Parse(error) => ("Parse Error", error.to_string()), + AppError::Request(error) => ("Request Error", error.to_string()), + AppError::ResourceNotFound => ( + "Resource Not Found", + "A requested resource was not found.".to_string(), + ), + AppError::Socket(error) => ( + "Socket Error", + match error { + SocketError::ParseJson(error) => error.to_string(), + SocketError::Utf8Decode(_) => "UTF-8 decode failed".to_string(), + SocketError::Gloo(web_socket_error) => web_socket_error.to_string(), + e => e.to_string(), + }, + ), + AppError::Js(error) => ("JS Error", error.to_string()), + _ => return, + }; + + self.push_error(error); + } + + fn first(&self) -> Option<(String, String)> { + self.errors().first().map(|x| x.clone()) + } + + fn last(&self) -> Option<(String, String)> { + self.errors().last().map(|x| x.clone()) + } + + fn pop(&mut self) -> Option<(String, String)> { + self.errors().pop() + } +} + +impl Default for Errors { + fn default() -> Self { + Self::new() + } +} diff --git a/packages/playground/playground/src/share_code.rs b/packages/playground/playground/src/share_code.rs index 4192cf4e10..01bf55359c 100644 --- a/packages/playground/playground/src/share_code.rs +++ b/packages/playground/playground/src/share_code.rs @@ -1,4 +1,4 @@ -use dioxus::signals::{Signal, Writable}; +use dioxus::prelude::*; use dioxus_document::eval; use model::{api::ApiClient, AppError, Project}; @@ -8,9 +8,16 @@ pub async fn copy_share_link( mut project: Signal, location: &str, ) -> Result<(), AppError> { - let share_code = project.write().share_project(api_client).await?; + let read_project = project.read(); + let shared_id = read_project.shared_id(); + let code = read_project.contents(); + // Drop the lock before running any async code. + drop(read_project); + let share_code = Project::share_project(shared_id, code, api_client).await?; - let formatted = format!("{}/shared/{}", location, share_code); + project.write().set_shared_id(share_code); + + let formatted = format!("{}/shared/{}", location, share_code.as_simple()); let e = eval( r#" const data = await dioxus.recv(); @@ -18,7 +25,8 @@ pub async fn copy_share_link( "#, ); - e.send(formatted)?; + e.send(&formatted)?; + router().push(formatted); Ok(()) } diff --git a/packages/playground/playground/src/theme.rs b/packages/playground/playground/src/theme.rs new file mode 100644 index 0000000000..ca0a0e6f86 --- /dev/null +++ b/packages/playground/playground/src/theme.rs @@ -0,0 +1,226 @@ +//! Theme utilities. +//! +//! Access the system's theme to use for common tasks such as automatically setting your app styling. +//! +//! Most apps will need to choose a default theme in the event of an error. +//! We recommend using either [`Result::unwrap_or`] or [`Result::unwrap_or_default`] to do this. +//! +//! #### Platform Support +//! Theme is available for Web, Windows, & Mac. Linux is unsupported and Android/iOS has not been tested. +//! +//! # Examples +//! An example of using the theme to determine which class to use. +//! ```rust +//! use dioxus::prelude::*; +//! use dioxus_window::theme::{use_system_theme, Theme}; +//! +//! #[component] +//! fn App() -> Element { +//! let theme = use_system_theme(); +//! +//! // Default to a light theme in the event of an error. +//! let class = match theme().unwrap_or(Theme::Light) { +//! Theme::Light => "bg-light", +//! Theme::Dark => "bg-dark", +//! }; +//! +//! rsx! { +//! div { +//! class: "{class}", +//! "the current theme is: {theme().unwrap_or(Theme::Light)}" +//! } +//! } +//! } +//! ``` +use dioxus::{core::provide_root_context, prelude::*}; +use std::{error::Error, fmt::Display}; + +/// A color theme. +/// +/// For any themes other than `light` and `dark`, a [`ThemeError::UnknownTheme`] will be returned. +/// We may be able to support custom themes in the future. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum Theme { + /// A light theme, better in direct sunlight. + #[default] + Light, + /// A dark theme, better for the night owls. + Dark, +} + +impl Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Light => write!(f, "light"), + Self::Dark => write!(f, "dark"), + } + } +} + +/// Possible theme errors. +#[derive(Debug, Clone, PartialEq)] +pub enum ThemeError { + /// Theme is not supported on this platform. + Unsupported, + /// Failed to get the system theme. + CheckFailed, + /// System returned an unknown theme. + UnknownTheme, +} + +impl Error for ThemeError {} +impl Display for ThemeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Unsupported => write!(f, "the current platform is not supported"), + Self::CheckFailed => write!( + f, + "the system returned an error while checking the color theme" + ), + Self::UnknownTheme => write!( + f, + "the system provided a theme other than `light` or `dark`" + ), + } + } +} + +type ThemeResult = Result; + +/// Get a signal to the system theme. +/// +/// On first run, the result will be [`ThemeError::Unsupported`]. This is to prevent hydration from failing. +/// After the client runs, the theme will be tracked and updated with accurate values. +/// +/// # Examples +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_window::theme::{use_system_theme, Theme}; +/// +/// #[component] +/// fn App() -> Element { +/// let theme = use_system_theme(); +/// +/// rsx! { +/// p { +/// "the current theme is: {theme().unwrap_or(Theme::Light)}" +/// } +/// } +/// } +/// ``` +pub fn use_system_theme() -> ReadSignal { + let mut system_theme = match try_use_context::>() { + Some(s) => s, + // This should only run once. + None => { + let signal = Signal::new_in_scope(Err(ThemeError::Unsupported), ScopeId::ROOT); + provide_root_context(signal) + } + }; + + // Only start the listener on the client. + use_effect(move || { + system_theme.set(get_theme()); + + #[cfg(target_arch = "wasm32")] + listen(system_theme); + }); + + use_hook(|| ReadSignal::new(system_theme)) +} + +// The listener implementation for wasm targets. +#[cfg(target_arch = "wasm32")] +fn listen(mut theme: Signal) { + use wasm_bindgen::{closure::Closure, JsCast}; + use web_sys::MediaQueryList; + + let Some(window) = web_sys::window() else { + theme.set(Err(ThemeError::Unsupported)); + return; + }; + + // Get the media query + let Ok(query) = window.match_media("(prefers-color-scheme: dark)") else { + theme.set(Err(ThemeError::CheckFailed)); + return; + }; + + let Some(query) = query else { + theme.set(Err(ThemeError::UnknownTheme)); + return; + }; + + // Listener that is called when the media query changes. + // https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/change_event + let listener = Closure::wrap(Box::new(move |query: MediaQueryList| { + match query.matches() { + true => theme.set(Ok(Theme::Dark)), + false => theme.set(Ok(Theme::Light)), + }; + }) as Box); + + let cb = listener.as_ref().clone(); + listener.forget(); + query.set_onchange(Some(cb.unchecked_ref())); +} + +/// Get the current theme. +/// +/// +/// **Note** +/// +/// This function will cause hydration to fail if not used inside an effect, task, or event handler. +/// +/// # Examples +/// +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_window::theme::{Theme, get_theme}; +/// +/// #[component] +/// fn App() -> Element { +/// let theme = use_signal(get_theme); +/// +/// let class_name = match theme().unwrap_or(Theme::Light) { +/// Theme::Dark => "dark-theme", +/// Theme::Light => "light-theme", +/// }; +/// +/// rsx! { +/// div { +/// style: "width: 100px; height: 100px;", +/// class: "{class_name}", +/// } +/// } +/// } +/// ``` +pub fn get_theme() -> ThemeResult { + #[cfg(target_arch = "wasm32")] + return get_theme_platform(); + + #[cfg(not(target_arch = "wasm32"))] + return Ok(Theme::Light); +} + +// The wasm implementation to get the system theme. +#[cfg(target_arch = "wasm32")] +fn get_theme_platform() -> ThemeResult { + let Some(window) = web_sys::window() else { + return Err(ThemeError::Unsupported); + }; + + // Check the color theme with a media query + let Some(query) = window + .match_media("(prefers-color-scheme: dark)") + .or(Err(ThemeError::CheckFailed))? + else { + return Err(ThemeError::UnknownTheme); + }; + + match query.matches() { + true => Ok(Theme::Dark), + false => Ok(Theme::Light), + } +} diff --git a/packages/playground/playground/src/ws.rs b/packages/playground/playground/src/ws.rs index 569e8e964c..6f5dcfa422 100644 --- a/packages/playground/playground/src/ws.rs +++ b/packages/playground/playground/src/ws.rs @@ -1,4 +1,10 @@ -use crate::{build::BuildStage, BuildState}; +use crate::{ + build::{BuildStage, BuildStateStoreExt, BuildStateStoreImplExt}, + hotreload::send_hot_reload, + BuildState, +}; +use dioxus::{signals::ReadableExt, stores::Store}; +use dioxus_devtools::HotReloadMsg; use futures::{SinkExt as _, StreamExt}; use gloo_net::websocket::futures::WebSocket; use model::*; @@ -38,17 +44,32 @@ impl Socket { } /// Handles a websocket message, returning true if further messages shouldn't be handled. -pub fn handle_message(mut build: BuildState, message: SocketMessage) -> bool { +pub fn handle_message(mut build: Store, message: SocketMessage) { match message { SocketMessage::BuildStage(stage) => build.set_stage(BuildStage::Building(stage)), - SocketMessage::QueuePosition(position) => build.set_queue_position(Some(position)), - SocketMessage::BuildFinished(result) => { - build.set_stage(BuildStage::Finished(result)); - return true; + SocketMessage::QueuePosition(position) => build.set_stage(BuildStage::Queued(position)), + SocketMessage::BuildFinished(BuildResult::Failed(failure)) => { + build.set_stage(BuildStage::Finished(Err(failure))); + } + SocketMessage::BuildFinished(BuildResult::Built(id)) => { + build.set_stage(BuildStage::Finished(Ok(id))); + } + SocketMessage::BuildFinished(BuildResult::HotPatched(patch)) => { + // Get the iframe to apply the patch to. + send_hot_reload(HotReloadMsg { + templates: Default::default(), + assets: Default::default(), + ms_elapsed: Default::default(), + for_pid: Default::default(), + for_build_id: Some(0), + jump_table: Some(patch), + }); + if let Some(id) = build.previous_build_id().cloned() { + build.set_stage(BuildStage::Finished(Ok(id))); + } } SocketMessage::BuildDiagnostic(diagnostic) => build.push_diagnostic(diagnostic), + SocketMessage::RateLimited(time) => build.set_stage(BuildStage::Waiting(time)), _ => {} } - - false } diff --git a/packages/playground/runner/src/main.rs b/packages/playground/runner/src/main.rs index 1df10b000a..7d25883c20 100644 --- a/packages/playground/runner/src/main.rs +++ b/packages/playground/runner/src/main.rs @@ -1,60 +1,51 @@ -// // TODO: Remove public folder with monaco in it (once manganis folder dir works) - -// use dioxus::logger::tracing::Level; -// use dioxus::prelude::*; -// use dioxus_playground::{Playground, PlaygroundUrls}; - -// #[cfg(not(feature = "real-server"))] -// const URLS: PlaygroundUrls = PlaygroundUrls { -// socket: "ws://localhost:3000/ws", -// server: "http://localhost:3000", -// location: "http://localhost:8080", -// }; - -// #[cfg(feature = "real-server")] -// const URLS: PlaygroundUrls = PlaygroundUrls { -// socket: "wss://docsite-playground.fly.dev/ws", -// server: "https://docsite-playground.fly.dev", -// location: "https://dioxuslabs.com/playground", -// }; - -// // Runner-only styling -// const MAIN_CSS: Asset = asset!("/src/main.css"); - -// #[derive(Routable, PartialEq, Clone)] -// enum Route { -// #[route("/")] -// DefaultPlayground {}, - -// #[route("/shared/:share_code")] -// SharePlayground { share_code: String }, -// } - -// fn main() { -// dioxus::logger::init(Level::INFO).expect("failed to start logger"); -// dioxus::launch(App); -// } - -// #[component] -// fn App() -> Element { -// rsx! { -// document::Link { rel: "stylesheet", href: MAIN_CSS } -// Router:: {} -// } -// } - -// #[component] -// fn DefaultPlayground() -> Element { -// rsx! { -// Playground { urls: URLS, class: "playground-container" } -// } -// } - -// #[component] -// fn SharePlayground(share_code: ReadOnlySignal>) -> Element { -// rsx! { -// Playground { urls: URLS, share_code, class: "playground-container" } -// } -// } - -fn main() {} +// TODO: Remove public folder with monaco in it (once manganis folder dir works) + +use dioxus::logger::tracing::Level; +use dioxus::prelude::*; +use dioxus_playground::{Playground, PlaygroundUrls}; + +#[cfg(not(feature = "real-server"))] +const URLS: PlaygroundUrls = PlaygroundUrls { + socket: "ws://localhost:3000/ws", + server: "http://localhost:3000", + location: "http://localhost:8080", +}; + +#[cfg(feature = "real-server")] +const URLS: PlaygroundUrls = PlaygroundUrls { + socket: "wss://docsite-playground-red-wildflower-209.fly.dev/ws", + server: "https://docsite-playground-red-wildflower-209.fly.dev", + location: "https://dioxuslabs.com/playground", +}; + +// Runner-only styling +const MAIN_CSS: Asset = asset!("/src/main.css"); + +#[derive(Routable, PartialEq, Clone)] +enum Route { + #[route("/", PlaygroundRoute)] + DefaultPlayground {}, + + #[route("/shared/:share_code", PlaygroundRoute)] + SharePlayground { share_code: String }, +} + +fn main() { + dioxus::logger::init(Level::INFO).expect("failed to start logger"); + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + rsx! { + document::Link { rel: "stylesheet", href: MAIN_CSS } + Router:: {} + } +} + +#[component] +fn PlaygroundRoute(share_code: ReadSignal>) -> Element { + rsx! { + Playground { urls: URLS, share_code, class: "playground-container" } + } +} diff --git a/packages/playground/server/Cargo.toml b/packages/playground/server/Cargo.toml index f5ea9c420e..48c57d829d 100644 --- a/packages/playground/server/Cargo.toml +++ b/packages/playground/server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "server" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] model = { workspace = true, features = ["server"] } @@ -12,6 +12,7 @@ serde_json = { workspace = true } dioxus-logger = { workspace = true } dioxus-dx-wire-format = { workspace = true } +dioxus-devtools-types = { workspace = true } axum = { workspace = true, features = ["ws", "macros"] } axum-client-ip = "1.1" @@ -21,9 +22,21 @@ tokio-util = { version = "0.7.11", features = ["futures-util"] } tower-http = { version = "0.5.2", features = ["compression-br", "cors", "fs"] } tower = { version = "0.4.13", features = ["buffer", "limit"] } reqwest = { workspace = true, features = ["json"] } +console-subscriber = { version = "0.4.1", optional = true } thiserror = { workspace = true } dioxus = { workspace = true, features = ["web"] } example-projects = { workspace = true } +tracing = "0.1.41" +tower-util = "0.3.1" +tracing-subscriber = "0.3.20" +tower_governor = "0.8.0" +governor = "0.10.1" +rustix = { version = "1.1.2", features = ["process"] } +dashmap = "6.1.0" +dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } + +[features] +tracing = ["tokio/tracing", "dep:console-subscriber"] diff --git a/packages/playground/server/src/app.rs b/packages/playground/server/src/app.rs index 70d455e5ef..f40865bd8b 100644 --- a/packages/playground/server/src/app.rs +++ b/packages/playground/server/src/app.rs @@ -1,28 +1,45 @@ //! Initialization of the server application and environment configurations. use crate::{ - build::{watcher::start_build_watcher, BuildCommand, BuildRequest}, + build::{BuildCommand, BuildRequest, watcher::start_build_watcher}, start_cleanup_services, }; use dioxus_logger::tracing::{info, warn}; +use governor::{ + Quota, RateLimiter, + clock::{QuantaClock, QuantaInstant}, + middleware::NoOpMiddleware, + state::keyed::DashMapStateStore, +}; use std::{ env, + fmt::Display, + net::IpAddr, + num::NonZeroU32, path::PathBuf, - sync::{atomic::AtomicBool, Arc}, + str::FromStr, + sync::{Arc, atomic::AtomicBool}, time::Duration, }; use tokio::{ - sync::{mpsc::UnboundedSender, Mutex}, + sync::{Mutex, mpsc::UnboundedSender}, time::Instant, }; +use uuid::Uuid; const DEFAULT_PORT: u16 = 3000; // Paths const DEFAULT_BUILD_TEMPLATE_PATH: &str = "./template"; -// Duration after built projects are created to be removed. -const DEFAULT_BUILT_CLEANUP_DELAY: Duration = Duration::from_secs(20); +/// Max size of the built directory before old projects are removed. +const DEFAULT_BUILT_DIR_SIZE: u64 = 1024 * 1024 * 1024; // 1 GB +/// Max memory usage of dx during a build before it is killed. +const DEFAULT_DX_MEMORY_LIMIT: u64 = 5 * 1024 * 1024 * 1024; // 5 GB +/// Max seconds a dx build can take before it is killed. +const DEFAULT_DX_BUILD_TIMEOUT: u64 = 5 * 60; // 5 minutes +/// Max size of the target directory before it is cleaned. +const DEFAULT_TARGET_DIR_SIZE: u64 = 3 * 1024 * 1024 * 1024; // 3 GB /// A group of environment configurations for the application. #[derive(Clone)] @@ -39,8 +56,19 @@ pub struct EnvVars { /// The path where built projects are temporarily stored. pub built_path: PathBuf, - /// The time after creation each built project should be removed. - pub built_cleanup_delay: Duration, + /// The max size of the built project directory before old projects are removed. + pub max_built_dir_size: u64, + + /// The max size of the target directory before it is cleaned. + pub max_target_dir_size: u64, + + /// The max memory limit for dx during a build. + #[cfg_attr(not(target_os = "linux"), allow(unused))] + pub dx_memory_limit: u64, + + /// The max seconds a dx build can take before it is killed. + #[cfg_attr(not(target_os = "linux"), allow(unused))] + pub dx_build_timeout: u64, /// The optional shutdown delay that specifies how many seconds after /// inactivity to shut down the server. @@ -57,6 +85,10 @@ impl EnvVars { let build_template_path = Self::get_build_template_path(); let shutdown_delay = Self::get_shutdown_delay(); let gist_auth_token = Self::get_gist_auth_token(); + let max_built_dir_size = Self::get_max_built_dir_size(); + let max_target_dir_size = Self::get_max_target_dir_size(); + let dx_memory_limit = Self::get_dx_memory_limit(); + let dx_build_timeout = Self::get_dx_build_timeout(); Self { production, @@ -68,60 +100,44 @@ impl EnvVars { PathBuf::from("./temp/") }, shutdown_delay, - built_cleanup_delay: DEFAULT_BUILT_CLEANUP_DELAY, + max_built_dir_size, + max_target_dir_size, + dx_memory_limit, + dx_build_timeout, gist_auth_token: gist_auth_token.unwrap_or_default(), } } + /// Get the path to the target dir + pub fn target_dir(&self) -> PathBuf { + self.build_template_path.join("target") + } + + /// Get the path to the built template hot patch cache + pub fn built_template_hotpatch_cache(&self, id: &Uuid) -> PathBuf { + self.target_dir() + .join("hotpatch_cache") + .join(id.to_string()) + } + /// Get the production environment variable. fn get_production_env() -> bool { - let production = env::var("PRODUCTION") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(false); - - info!("is the server is running in production? {production}"); - production + get_env_or("PRODUCTION", false) } /// Get the serve port from environment or default. fn get_port_env() -> u16 { - let mut port = DEFAULT_PORT; - match env::var("PORT") { - Ok(v) => { - port = v - .parse() - .expect("the `PORT` environment variable should be a number") - } - Err(_) => info!( - "`PORT` environment variable not set; defaulting to `{}`", - port - ), - } - - port + get_env_or("PORT", DEFAULT_PORT) } /// Get the build template path from environment or default. fn get_build_template_path() -> PathBuf { - let mut build_template_path = PathBuf::from(DEFAULT_BUILD_TEMPLATE_PATH); - match env::var("BUILD_TEMPLATE_PATH") { - Ok(v) => build_template_path = PathBuf::from(v), - Err(_) => info!( - "`BUILD_TEMPLATE_PATH` environment variable is not set; defaulting to `{:?}`", - build_template_path - ), - } - - build_template_path + get_env_parsed("BUILD_TEMPLATE_PATH").unwrap_or_else(|| DEFAULT_BUILD_TEMPLATE_PATH.into()) } /// Get the server shutdown delay from the environment. fn get_shutdown_delay() -> Option { - let shutdown_delay = env::var("SHUTDOWN_DELAY") - .ok() - .and_then(|v| v.parse().ok()) - .map(Duration::from_secs); + let shutdown_delay = get_env_parsed::("SHUTDOWN_DELAY").map(Duration::from_secs); if shutdown_delay.is_none() { warn!("`SHUTDOWN_DELAY` environment variable is not set; the server will not turn off") @@ -132,21 +148,46 @@ impl EnvVars { /// Get the GitHub Gists authentication token from the environment. fn get_gist_auth_token() -> Option { - let gist_auth_token = env::var("GIST_AUTH_TOKEN").ok(); + get_env_parsed("GIST_AUTH_TOKEN") + } - if gist_auth_token.is_none() { - warn!("`GIST_AUTH_TOKEN` environment variable is not set") - } + /// Get the max size of the built directory from the environment or default. + fn get_max_built_dir_size() -> u64 { + get_env_or("MAX_BUILT_DIR_SIZE", DEFAULT_BUILT_DIR_SIZE) + } + + /// Get the max size of the target directory from the environment or default. + fn get_max_target_dir_size() -> u64 { + get_env_or("MAX_TARGET_DIR_SIZE", DEFAULT_TARGET_DIR_SIZE) + } - gist_auth_token + /// Get the max memory limit for dx during a build from the environment or default. + fn get_dx_memory_limit() -> u64 { + get_env_or("DX_MEMORY_LIMIT", DEFAULT_DX_MEMORY_LIMIT) + } + + /// Get the max seconds a dx build can take before it is killed from the environment or default. + fn get_dx_build_timeout() -> u64 { + get_env_or("DX_BUILD_TIMEOUT", DEFAULT_DX_BUILD_TIMEOUT) } } +fn get_env_parsed(key: &str) -> Option { + env::var(key).ok().and_then(|v| v.parse().ok()) +} + +fn get_env_or(key: &str, default: F) -> F { + get_env_parsed(key).unwrap_or_else(|| { + info!("`{key}` environment variable not set; defaulting to `{default}`"); + default + }) +} + /// The state of the server application. #[derive(Clone)] pub struct AppState { /// The environment configuration. - pub env: EnvVars, + pub env: Arc, /// The time instante since the last request. pub last_request_time: Arc>, @@ -157,10 +198,11 @@ pub struct AppState { /// Prevents the server from shutting down during an active build. pub is_building: Arc, - /// A list of connected sockets by ip. Used to disallow extra socket connections. - pub _connected_sockets: Arc>>, - pub reqwest_client: reqwest::Client, + + pub build_govener: Arc< + RateLimiter, QuantaClock, NoOpMiddleware>, + >, } impl AppState { @@ -168,29 +210,33 @@ impl AppState { pub async fn new() -> Self { let mut env = EnvVars::new().await; - // Build the app state - let is_building = Arc::new(AtomicBool::new(false)); - let build_queue_tx = start_build_watcher(env.clone(), is_building.clone()); - // Get prebuild arg - let prebuild = std::env::args() - .collect::>() - .get(1) - .map(|x| x == "--prebuild") - .unwrap_or(false); + let prebuild = std::env::args().any(|x| x == "--prebuild"); if prebuild { info!("server is prebuilding"); env.shutdown_delay = Some(Duration::from_secs(1)); } + let env = Arc::new(env); + + // Build the app state + let is_building = Arc::new(AtomicBool::new(false)); + let build_queue_tx = start_build_watcher(env.clone(), is_building.clone()); + + let build_govener = Arc::new(RateLimiter::keyed( + Quota::with_period(Duration::from_secs(5)) + .expect("period is non-zero") + .allow_burst(const { NonZeroU32::new(2).unwrap() }), + )); + let state = Self { env, build_queue_tx, last_request_time: Arc::new(Mutex::new(Instant::now())), is_building, - _connected_sockets: Arc::new(Mutex::new(Vec::new())), reqwest_client: reqwest::Client::new(), + build_govener, }; // Queue the examples to be built on startup. @@ -202,6 +248,7 @@ impl AppState { let _ = state.build_queue_tx.send(BuildCommand::Start { request: BuildRequest { id: project.id(), + previous_build_id: None, project: project.clone(), ws_msg_tx: tx.clone(), }, diff --git a/packages/playground/server/src/build/builder.rs b/packages/playground/server/src/build/builder.rs index c1939485d3..8c8df1ea9d 100644 --- a/packages/playground/server/src/build/builder.rs +++ b/packages/playground/server/src/build/builder.rs @@ -1,43 +1,76 @@ use super::{BuildError, BuildRequest}; use crate::app::EnvVars; use crate::build::{BuildMessage, CliMessage}; +use dioxus::subsecond::JumpTable; use dioxus_dx_wire_format::StructuredOutput; use dioxus_logger::tracing; use dioxus_logger::tracing::debug; use fs_extra::dir::CopyOptions; use model::{BuildStage, CargoDiagnostic}; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::future::pending; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::process::{Child, Command}; +use std::process::{ExitStatus, Stdio}; use std::sync::Arc; -use tokio::io::{AsyncBufReadExt as _, BufReader}; -use tokio::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::fs; use tokio::task::JoinHandle; -use tokio::{fs, select}; +use tracing::{trace, warn}; -const BUILD_ID_ID: &str = "{BUILD_ID}"; +const BUILD_ID_TEMPLATE: &str = "{BUILD_ID}"; -// TODO: We need some way of cleaning up any stopped builds. /// The builder provides a convenient interface for controlling builds running in another task. pub struct Builder { - template_path: PathBuf, - built_path: PathBuf, + env: Arc, is_building: Arc, current_build: Option, - task: JoinHandle>, + task: Option, BuildError>>>, } impl Builder { - pub fn new(env: EnvVars, is_building: Arc) -> Self { + /// Create a new builder + pub fn new(env: Arc, is_building: Arc) -> Self { Self { - template_path: env.build_template_path, - built_path: env.built_path, + env, is_building, current_build: None, - task: tokio::spawn(std::future::pending()), + task: None, } } + /// Make sure the components are initialized + pub async fn update_component_library(build_template_path: &Path) -> Result<(), BuildError> { + // Update the component library cache + let update_status = tokio::process::Command::new("dx") + .arg("components") + .arg("update") + .current_dir(build_template_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await?; + if !update_status.success() { + return Err(BuildError::DxFailed(update_status.code())); + } + // Add all components to the template project + let status = tokio::process::Command::new("dx") + .arg("components") + .arg("add") + .arg("calendar") + .arg("--force") + .current_dir(build_template_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await?; + if !status.success() { + return Err(BuildError::DxFailed(status.code())); + } + + Ok(()) + } + /// Start a new build, cancelling any ongoing builds. pub fn start(&mut self, request: BuildRequest) { let _ = request.ws_msg_tx.send(BuildMessage::QueuePosition(0)); @@ -45,33 +78,32 @@ impl Builder { self.stop_current(); self.is_building.store(true, Ordering::SeqCst); self.current_build = Some(request.clone()); - self.task = tokio::spawn(build( - self.template_path.clone(), - self.built_path.clone(), - request, - )); + self.task = Some(tokio::spawn(build(self.env.clone(), request))); } /// Stop the current build. pub fn stop_current(&mut self) { - self.task.abort(); - self.task = tokio::spawn(std::future::pending()); + if let Some(task) = self.task.take() { + task.abort(); + } self.current_build = None; self.is_building.store(false, Ordering::SeqCst); } /// Wait for the current build to finish. - pub async fn finished(&mut self) -> Result { + pub async fn finished(&mut self) -> Result<(BuildRequest, Option), BuildError> { // Ensure we don't poll a completed task. - if self.task.is_finished() { - self.stop_current(); + if let Some(task) = &mut self.task { + if task.is_finished() { + self.stop_current(); + } else { + // Make progress on the build task. + let response = task.await??; + let request = self.current_build.take().ok_or(BuildError::NotStarted)?; + return Ok((request, response)); + } } - - // Make progress on the build task. - let task = &mut self.task; - task.await??; - - self.current_build.take().ok_or(BuildError::NotStarted) + pending().await } /// Check if the builder has an ongoing build. @@ -85,31 +117,39 @@ impl Builder { } } -/// Run the steps to produce a build for a [`BuildRequest`] -async fn build( - template_path: PathBuf, - built_path: PathBuf, - request: BuildRequest, -) -> Result<(), BuildError> { +/// Run the steps to produce a build or patch for a [`BuildRequest`] +async fn build(env: Arc, request: BuildRequest) -> Result, BuildError> { + let built_path = &env.built_path; + let template_path = &env.build_template_path; + // If the project already exists, don't build it again. if std::fs::exists(built_path.join(request.id.to_string())).unwrap_or_default() { tracing::trace!("Skipping build for {request:?} since it already exists"); - return Ok(()); + return Ok(None); } - setup_template(&template_path, &request).await?; - dx_build(&template_path, &request).await?; - tracing::trace!("Noving build from {template_path:?} to {built_path:?}"); - move_to_built(&template_path, &built_path, &request).await?; + // Check if we need to clean up old builds before starting a new one. + if let Err(e) = super::cleanup::check_cleanup(&env).await { + warn!("failed to clean built projects: {e}"); + } - Ok(()) + // Build or hotpatch depending on the build state + let patch = dx_build(&request, env.clone()).await?; + // Move the built project or hotpatch binary into the built projects folder. + move_to_built(template_path, built_path, &request, patch.is_some()).await?; + + Ok(patch) } /// Resets the template with values for the next build. -async fn setup_template(template_path: &Path, request: &BuildRequest) -> Result<(), BuildError> { - let snippets_from_copy = [ - template_path.join("snippets/Cargo.toml"), - template_path.join("snippets/Dioxus.toml"), +async fn setup_template( + template_path: &Path, + request: &BuildRequest, + patch: bool, +) -> Result<(), BuildError> { + let snippets_templates = [ + include_str!("../../template/snippets/Cargo.toml"), + include_str!("../../template/snippets/Dioxus.toml"), ]; // New locations @@ -119,13 +159,25 @@ async fn setup_template(template_path: &Path, request: &BuildRequest) -> Result< ]; // Enumerate over a list of paths to copy and copies them to the new location while modifying any template strings. - for (i, path) in snippets_from_copy.iter().enumerate() { + for (i, contents) in snippets_templates.iter().enumerate() { let new_path = &snippets_to_copy[i]; - let contents = fs::read_to_string(path).await?; - let contents = contents.replace(BUILD_ID_ID, &request.id.to_string()); + let contents = contents.replace( + BUILD_ID_TEMPLATE, + &request + .previous_build_id + .filter(|_| patch) + .unwrap_or(request.id) + .to_string(), + ); fs::write(new_path, contents).await?; } + // Create the src directory if it doesn't exist + let src_path = template_path.join("src"); + if !src_path.exists() { + fs::create_dir_all(&src_path).await?; + } + // Write the user's code to main.rs fs::write( template_path.join("src/main.rs"), @@ -133,99 +185,279 @@ async fn setup_template(template_path: &Path, request: &BuildRequest) -> Result< ) .await?; + // If the component library doesn't exist, create it + let component_folder_path = template_path.join("src").join("components"); + if !component_folder_path.exists() { + Builder::update_component_library(template_path).await?; + } + Ok(()) } -/// Run the build command provided by the DX CLI. -/// Returns if DX built the project successfully. -async fn dx_build(template_path: &PathBuf, request: &BuildRequest) -> Result<(), BuildError> { - let mut child = Command::new("dx") - .arg("build") - .arg("--platform") - .arg("web") - .arg("--json-output") - .arg("--verbose") - .arg("--trace") - .current_dir(template_path) +/// Start a process with limited access to the environment and resources +fn start_limited_process(mut command: Command, env: &EnvVars) -> Result { + // We want to limit the environment variables passed to dx to only those needed for Rust. + // This prevents leaking any sensitive information to the build process which could be read with env! + let filtered_vars = std::env::vars().filter(|(k, _)| { + let allowed = ["RUST_VERSION", "RUSTUP_HOME", "CARGO_HOME", "PATH", "HOME"]; + allowed.contains(&k.as_str()) + }); + + let child = command + .env_clear() + .envs(filtered_vars) + .current_dir(&env.build_template_path) + .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; + set_dx_limits(&child, env); + + Ok(child) +} + +/// Run the build command provided by the DX CLI. +/// Returns if DX built the project successfully. +async fn dx_build( + request: &BuildRequest, + env: Arc, +) -> Result, BuildError> { + // If there is a previous build, get the cache dir for that build if it exists + let previous_build_cache_dir = request + .previous_build_id + .map(|id| env.built_template_hotpatch_cache(&id)) + .filter(|p| p.exists()); + + // Ff there is a previous cache, try to use that to hot patch the build + if let Some(cache_dir) = previous_build_cache_dir { + let cache_dir = cache_dir.canonicalize()?; + let result = start_dx_build(request, env.clone(), &cache_dir, true).await; + if let Ok(Some(patch)) = result { + return Ok(Some(patch)); + } else { + trace!("hotpatch build failed, falling back to full build"); + } + } + + // If there is no previous cache, or the hotpatch failed, get the cache dir for a new build + let cache_dir = env.built_template_hotpatch_cache(&request.id); + + // fallback to a full build + if let Err(err) = std::fs::create_dir_all(&cache_dir) { + warn!("failed to create hotpatch cache dir {cache_dir:?}: {err}"); + } + let cache_dir = cache_dir.canonicalize()?; + start_dx_build(request, env, &cache_dir, false).await +} + +async fn start_dx_build( + request: &BuildRequest, + env: Arc, + cache_dir: &Path, + patch: bool, +) -> Result, BuildError> { + setup_template(&env.build_template_path, request, patch).await?; + let mut command = Command::new("dx"); + if patch { + // If we are patching, use the hotpatch tool instead of doing a full build + command + .arg("tools") + .arg("hotpatch") + .arg("--web") + .arg("--session-cache-dir") + .arg(cache_dir) + .arg("--aslr-reference") + .arg("0") + .arg("--json-output"); + } else { + command + .arg("build") + .arg("--web") + // We always do a fat binary build to allow hotpatching later. + .arg("--fat-binary") + // Each build gets its own session dir we re-use later for hotpatching. + .arg("--session-cache-dir") + .arg(cache_dir) + .arg("--json-output"); + } + tracing::info!("running dx command: {:?}", command); + + let mut child = start_limited_process(command, &env)?; + + if patch { + // If there is a artifacts cache, we can pipe it to dx for hot patching. + let artifacts_cache = cache_dir.join("artifacts.json"); + let artifacts_cache = std::fs::read_to_string(artifacts_cache)?; + if let Some(mut stdin) = child.stdin.take() { + use std::io::Write; + stdin.write_all(artifacts_cache.as_bytes())?; + stdin.flush()?; + } + } + + let BuildResult { + logs, + patch, + status, + } = tokio::task::spawn_blocking({ + let request = request.clone(); + move || process_build_messages(&mut child, &env, &request) + }) + .await?; + + // Check if the build was successful. + if let Ok(Some(code)) = status.as_ref().map(ExitStatus::code) { + if code == 0 { + return Ok(patch); + } else { + // Dump logs in debug. + for log in logs { + debug!("{log}"); + } + return Err(BuildError::DxFailed(Some(code))); + } + } + + // Dump logs in debug. + for log in logs { + debug!("{log}"); + } + + Err(BuildError::DxFailed(None)) +} + +struct BuildResult { + logs: Vec, + patch: Option, + status: std::io::Result, +} + +/// Process the stdout and stderr of a dx build process, returning the logs and any hotpatch jump table +fn process_build_messages(child: &mut Child, env: &EnvVars, request: &BuildRequest) -> BuildResult { + let stderr = child.stderr.take().expect("dx stdout should exist"); let stdout = child.stdout.take().expect("dx stdout should exist"); let mut stdout_reader = BufReader::new(stdout).lines(); + let mut stderr_reader = BufReader::new(stderr).lines(); let mut logs = Vec::new(); + let mut patch = None; - loop { - select! { - // Read stdout lines from DX. - result = stdout_reader.next_line() => { - let Ok(Some(line)) = result else { - continue; - }; + while let Some(Ok(line)) = stdout_reader.next() { + logs.push(line.clone()); + patch = patch.or(process_dx_message(env, request, line)); + } - logs.push(line.clone()); - process_dx_message(request, line); - } - // Wait for the DX process to exit. - status = child.wait() => { - // Check if the build was successful. - let exit_code = status.map(|c| c.code()); - if let Ok(Some(code)) = exit_code { - if code == 0 { - break; - } else { - // Dump logs in debug. - for log in logs { - debug!("{log}"); - } - - return Err(BuildError::DxFailed(Some(code))); - } - } - return Err(BuildError::DxFailed(None)); - } - } + while let Some(Ok(line)) = stderr_reader.next() { + logs.push(line.clone()); + warn!("dx stderr: {line}"); } - Ok(()) + let status = child.wait(); + + BuildResult { + logs, + patch, + status, + } } -/// Process a JSON-formatted message from the DX CLI, returning nothing on error. +/// Limit a child process's resource usage. This prevents extremely long builds or excessive memory usage from crashing the server. +#[allow(unused)] +fn set_dx_limits(process: &Child, env: &EnvVars) { + #[cfg(any(target_os = "android", target_os = "linux"))] + { + use rustix::process::{Resource, Rlimit}; + let id = rustix::process::Pid::from_child(process); + let memory_limit = Rlimit { + current: Some(env.dx_memory_limit), + maximum: Some(env.dx_memory_limit), + }; + + if let Err(err) = rustix::process::prlimit(Some(id), Resource::As, memory_limit) { + warn!("failed to set memory limit for dx process {id}: {err}"); + } + + let cpu_limit = Rlimit { + current: Some(env.dx_build_timeout), + maximum: Some(env.dx_build_timeout), + }; + + if let Err(err) = rustix::process::prlimit(Some(id), Resource::Cpu, cpu_limit) { + warn!("failed to set cpu time limit for dx process {id}: {err}"); + } + } +} + +/// Process a JSON-formatted message from the DX CLI, returning a JumpTable if a hot-patch was produced. /// /// We don't care if this errors as it is human-readable output which the playground doesn't depend on for build status. -fn process_dx_message(request: &BuildRequest, message: String) { +fn process_dx_message(env: &EnvVars, request: &BuildRequest, message: String) -> Option { // We parse the tracing json log and if it contains a json field, we parse that as StructuredOutput. let result = serde_json::from_str::(&message) - .ok() - .and_then(|v| v.json) - .and_then(|json| serde_json::from_str::(&json).ok()); + .and_then(|m| serde_json::from_str::(&m.json)); - let Some(output) = result else { - return; + let Ok(output) = result else { + return None; }; - let _ = match output { + let from_main_crate = |diagnostic: &CargoDiagnostic| { + // Only send diagnostic for the main.rs file of the current build + diagnostic.target_crate == Some(format!("play-{}", request.id)) + && diagnostic + .spans + .iter() + .any(|s| s.file_name == "src/main.rs") + }; + + _ = match output { StructuredOutput::BuildUpdate { stage } => { let stage = BuildStage::from(stage); request.ws_msg_tx.send(BuildMessage::Building(stage)) } StructuredOutput::CargoOutput { message } => { let Ok(diagnostic) = CargoDiagnostic::try_from(message) else { - return; + return None; }; - // Don't send any diagnostics for dependencies. - if diagnostic.target_crate != format!("play-{}", request.id) { - return; + if !from_main_crate(&diagnostic) { + return None; } request .ws_msg_tx .send(BuildMessage::CargoDiagnostic(diagnostic)) } + StructuredOutput::RustcOutput { message } => { + let diagnostic = CargoDiagnostic::from(message); + + if !from_main_crate(&diagnostic) { + return None; + } + + request + .ws_msg_tx + .send(BuildMessage::CargoDiagnostic(diagnostic)) + } + StructuredOutput::BuildsFinished { client, .. } => { + let cache_dir = env.built_template_hotpatch_cache(&request.id); + let artifacts_cache = cache_dir.join("artifacts.json"); + if let Err(err) = std::fs::write( + &artifacts_cache, + serde_json::to_string(&client).unwrap_or_default(), + ) { + warn!("failed to write artifacts cache {artifacts_cache:?}: {err}"); + } + + Ok(()) + } + StructuredOutput::Hotpatch { jump_table, .. } => { + return Some(jump_table); + } _ => Ok(()), }; + + None } /// Moves the project built by `dx` to the final location for serving. @@ -233,8 +465,13 @@ async fn move_to_built( template_path: &Path, built_path: &Path, request: &BuildRequest, + patched: bool, ) -> Result<(), BuildError> { - let id_string = request.id.to_string(); + let id_string = request + .previous_build_id + .filter(|_| patched) + .unwrap_or(request.id) + .to_string(); // The path to the built project from DX let play_build_id = format!("play-{}", &id_string); @@ -251,14 +488,15 @@ async fn move_to_built( // Delete the built project in the target directory to prevent a storage leak. // We use `spawn_blocking` to batch call `std::fs` as recommended by Tokio. tokio::task::spawn_blocking::<_, Result<(), BuildError>>(move || { - // Rename to be the build id - let built_project = debug_web.join(&id_string); - std::fs::rename(&public_folder, &built_project)?; - + _ = std::fs::create_dir_all(&built_path); // Copy to the built project folder for serving. let options = CopyOptions::new().overwrite(true); - fs_extra::dir::move_dir(&built_project, &built_path, &options)?; - std::fs::remove_dir_all(&built_project_parent)?; + fs_extra::dir::copy(&public_folder, &built_path, &options)?; + let out_dir = built_path.join(id_string); + // Remove the old output dir if it exists + _ = std::fs::remove_dir_all(&out_dir); + // rename to the build id + std::fs::rename(built_path.join("public"), out_dir)?; Ok(()) }) .await??; diff --git a/packages/playground/server/src/build/cleanup.rs b/packages/playground/server/src/build/cleanup.rs new file mode 100644 index 0000000000..10e425f167 --- /dev/null +++ b/packages/playground/server/src/build/cleanup.rs @@ -0,0 +1,127 @@ +use std::io; + +use crate::app::EnvVars; + +/// Check and cleanup any expired built projects or the target dir +pub async fn check_cleanup(env: &EnvVars) -> Result<(), io::Error> { + check_project_cleanup(env).await?; + check_target_cleanup(env).await?; + Ok(()) +} + +/// Check and cleanup the target dir if it exceeds the max size. The hot reloading +/// cache is inside the target dir so it will also be cleared when the incremental +/// artifacts are removed. +async fn check_target_cleanup(env: &EnvVars) -> Result<(), io::Error> { + let target_path = env.target_dir(); + // If we just cleaned or this is the first run, the target dir may not exist. + if !target_path.exists() { + return Ok(()); + } + + let target_size = dir_size(&target_path).await?; + + if target_size > env.max_target_dir_size { + tokio::fs::remove_dir_all(&target_path).await?; + } + + Ok(()) +} + +/// Check and cleanup any expired built projects. This tends to be much smaller +/// than the target dir, but it can grow large over time as patches accumulate. +async fn check_project_cleanup(env: &EnvVars) -> Result<(), io::Error> { + // If we just cleaned or this is the first run, the built dir may not exist. + if !env.built_path.exists() { + return Ok(()); + } + + let mut dir = tokio::fs::read_dir(&env.built_path).await?; + let mut dirs_with_size = Vec::new(); + + // Go through the project directory and find the size and time modified for each project dir. + while let Some(item) = dir.next_entry().await? { + let path = item.path(); + let Some(filename) = path.file_name() else { + continue; + }; + let filename = filename.to_string_lossy().to_string(); + + let metadata = item.metadata().await; + let time_elapsed = metadata + .and_then(|m| m.modified()) + .and_then(|c| c.elapsed().map_err(io::Error::other)); + let size = dir_size(&path).await; + if let (Ok(time_elapsed), Ok(size)) = (time_elapsed, size) { + dirs_with_size.push((filename, item, time_elapsed, size)); + } else { + tracing::trace!("skipping cleanup of {filename} due to error reading metadata") + } + } + + // Find the total size of the built directory. + let total_size: u64 = dirs_with_size.iter().map(|(_, _, _, size)| *size).sum(); + // If it exceeds the max, sort by oldest and remove until under the limit. + if total_size > env.max_built_dir_size { + // Sort by oldest first + dirs_with_size.sort_by_key(|(_, _, time_elapsed, _)| *time_elapsed); + let mut size = total_size; + + // Remove oldest dirs until under the max size. + for (pathname, item, _, dir_size) in dirs_with_size { + if size <= env.max_built_dir_size { + break; + } + + let path = item.path(); + // Always cache the examples - only remove the patches + if example_projects::get_example_projects() + .iter() + .any(|p| p.id().to_string() == pathname) + { + // Find all files in the wasm folder that contain patch and remove them + let wasm_path = path.join("wasm"); + if wasm_path.exists() { + let mut wasm_dir = tokio::fs::read_dir(&wasm_path).await?; + while let Some(entry) = wasm_dir.next_entry().await? { + let entry_path = entry.path(); + if let Some(name) = entry_path.file_name() + && name.to_string_lossy().contains("patch") + { + let patch_size = entry.metadata().await.map(|m| m.len()).unwrap_or(0); + _ = tokio::fs::remove_file(&entry_path).await; + size -= patch_size; + } + } + } + } else { + _ = tokio::fs::remove_dir_all(&path).await; + size -= dir_size; + } + } + } + + Ok(()) +} + +/// Recursively calculate the size of a directory. +async fn dir_size(path: &std::path::Path) -> Result { + let mut size = 0; + let mut dirs = vec![path.to_path_buf()]; + + while let Some(dir) = dirs.pop() { + let mut entries = tokio::fs::read_dir(&dir).await?; + + while let Some(entry) = entries.next_entry().await.ok().flatten() { + let metadata = entry.metadata().await?; + + if metadata.is_dir() { + dirs.push(entry.path()); + } else { + size += metadata.len(); + } + } + } + + Ok(size) +} diff --git a/packages/playground/server/src/build/mod.rs b/packages/playground/server/src/build/mod.rs index cc04a14223..f8d7dddb7f 100644 --- a/packages/playground/server/src/build/mod.rs +++ b/packages/playground/server/src/build/mod.rs @@ -1,3 +1,4 @@ +use model::BuildResult; use model::CargoDiagnostic; use model::Project; use std::io; @@ -6,6 +7,7 @@ use tokio::{sync::mpsc::UnboundedSender, task::JoinError}; use uuid::Uuid; pub mod builder; +pub mod cleanup; pub mod watcher; /// A build command which allows consumers of the builder api to submit and stop builds. @@ -19,6 +21,7 @@ pub enum BuildCommand { #[derive(Debug, Clone)] pub struct BuildRequest { pub id: Uuid, + pub previous_build_id: Option, pub project: Project, pub ws_msg_tx: UnboundedSender, } @@ -28,14 +31,21 @@ pub struct BuildRequest { pub enum BuildMessage { Building(model::BuildStage), CargoDiagnostic(CargoDiagnostic), - Finished(Result), + Finished(BuildResult), QueuePosition(usize), } +impl BuildMessage { + /// Check if the build is done + pub fn is_done(&self) -> bool { + matches!(self, Self::Finished(_)) + } +} + /// The DX CLI serves parseable JSON output with the regular tracing message and a parseable "json" field. #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct CliMessage { - json: Option, + json: String, } /// Build failed to complete. diff --git a/packages/playground/server/src/build/watcher.rs b/packages/playground/server/src/build/watcher.rs index dbad085ee8..81d8392d3b 100644 --- a/packages/playground/server/src/build/watcher.rs +++ b/packages/playground/server/src/build/watcher.rs @@ -1,9 +1,10 @@ -use super::{builder::Builder, BuildCommand, BuildError, BuildMessage, BuildRequest}; +use super::{BuildCommand, BuildError, BuildMessage, BuildRequest, builder::Builder}; use crate::app::EnvVars; +use dioxus::subsecond::JumpTable; use std::{ collections::VecDeque, error::Error as _, - sync::{atomic::AtomicBool, Arc}, + sync::{Arc, atomic::AtomicBool}, }; use tokio::{ select, @@ -16,13 +17,14 @@ use uuid::Uuid; /// The build watcher receives [`BuildCommand`]s through a channel and handles /// the build queue, providing queue positions, and stopping/cancelling builds. pub fn start_build_watcher( - env: EnvVars, + env: Arc, is_building: Arc, ) -> UnboundedSender { let (tx, mut rx) = mpsc::unbounded_channel(); + let mut builder = Builder::new(env, is_building); + tokio::spawn(async move { - let mut builder = Builder::new(env, is_building); let mut pending_builds = VecDeque::new(); loop { @@ -69,19 +71,19 @@ fn start_build( fn stop_build(builder: &mut Builder, pending_builds: &mut VecDeque, id: Uuid) { // Check if the ongoing build is the cancelled build. let current_build_id = builder.current_build().map(|b| b.id); - if let Some(current_build_id) = current_build_id { - if id == current_build_id { - builder.stop_current(); - - // Start the next build request. - let next_request = pending_builds.pop_front(); - if let Some(request) = next_request { - builder.start(request); - } - - update_queue_positions(pending_builds); - return; + if let Some(current_build_id) = current_build_id + && id == current_build_id + { + builder.stop_current(); + + // Start the next build request. + let next_request = pending_builds.pop_front(); + if let Some(request) = next_request { + builder.start(request); } + + update_queue_positions(pending_builds); + return; } // Try finding the build in the queue @@ -114,26 +116,33 @@ fn stop_build(builder: &mut Builder, pending_builds: &mut VecDeque fn handle_finished_build( builder: &mut Builder, pending_builds: &mut VecDeque, - build_result: Result, + build_result: Result<(BuildRequest, Option), BuildError>, ) { // Tell the socket the result of their build. - let _ = match build_result { - Ok(request) => { - dioxus::logger::tracing::trace!(request = ?request, "build finished"); - request - .ws_msg_tx - .send(BuildMessage::Finished(Ok(request.id))) - } - Err(e) => { - dioxus::logger::tracing::warn!(err = ?e, src = ?e.source(), "build failed"); - match builder.current_build() { - Some(request) => request - .ws_msg_tx - .send(BuildMessage::Finished(Err(e.to_string()))), - None => Ok(()), + let _ = + match build_result { + Ok((request, response)) => { + dioxus::logger::tracing::trace!(request = ?request, "build finished"); + if let Some(patch) = response { + request.ws_msg_tx.send(BuildMessage::Finished( + crate::build::BuildResult::HotPatched(patch), + )) + } else { + request.ws_msg_tx.send(BuildMessage::Finished( + crate::build::BuildResult::Built(request.id), + )) + } } - } - }; + Err(e) => { + dioxus::logger::tracing::warn!(err = ?e, src = ?e.source(), "build failed"); + match builder.current_build() { + Some(request) => request.ws_msg_tx.send(BuildMessage::Finished( + crate::build::BuildResult::Failed(e.to_string()), + )), + None => Ok(()), + } + } + }; // Start the next build. let next_request = pending_builds.pop_front(); diff --git a/packages/playground/server/src/built.rs b/packages/playground/server/src/built.rs new file mode 100644 index 0000000000..0bf1067fca --- /dev/null +++ b/packages/playground/server/src/built.rs @@ -0,0 +1,52 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use dioxus_logger::tracing::warn; +use std::path::PathBuf; +use tower_util::ServiceExt; +use uuid::Uuid; + +use crate::app::AppState; + +/// Handle providing temporary built wasm assets. +pub async fn serve_built_index( + State(state): State, + Path(build_id): Path, + request: axum::extract::Request, +) -> impl IntoResponse { + let path = state.env.built_path.join(build_id.to_string()); + + let index_path = path.join("index.html"); + + // Serve the file with tower_http + tower_http::services::ServeFile::new(index_path) + .oneshot(request) + .await + .map_err(|e| { + warn!(err = ?e, build_id = ?build_id, "failed to serve built project file:"); + (StatusCode::NOT_FOUND, "not found") + }) +} + +pub async fn serve_other_built( + State(state): State, + Path((build_id, file_path)): Path<(Uuid, PathBuf)>, + request: axum::extract::Request, +) -> impl IntoResponse { + let path = state + .env + .built_path + .join(build_id.to_string()) + .join(file_path); + + // Serve the file with tower_http + tower_http::services::ServeFile::new(path) + .oneshot(request) + .await + .map_err(|e| { + warn!(err = ?e, build_id = ?build_id, "failed to serve built project file:"); + (StatusCode::NOT_FOUND, "not found") + }) +} diff --git a/packages/playground/server/src/main.rs b/packages/playground/server/src/main.rs index d5b2965a3e..ba20376366 100644 --- a/packages/playground/server/src/main.rs +++ b/packages/playground/server/src/main.rs @@ -1,193 +1,164 @@ -// use app::AppState; -// use axum::{ -// error_handling::HandleErrorLayer, -// extract::{Request, State}, -// http::StatusCode, -// middleware::{self, Next}, -// response::{Redirect, Response}, -// routing::{get, post}, -// BoxError, Router, -// }; -// use axum_client_ip::SecureClientIpSource; -// use dioxus_logger::tracing::{error, info, warn, Level}; -// use share::{get_shared_project, share_project}; -// use std::{io, net::SocketAddr, sync::atomic::Ordering, time::Duration}; -// use tokio::{net::TcpListener, select, time::Instant}; -// use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder}; -// use tower_http::{compression::CompressionLayer, cors::CorsLayer}; - -// mod app; -// mod build; -// mod serve; -// mod share; -// mod ws; - -// /// Rate limiter configuration. -// /// How many requests each user should get within a time period. -// const REQUESTS_PER_INTERVAL: u64 = 30; -// /// The period of time after the request limit resets. -// const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(60); - -// #[tokio::main] -// async fn main() { -// dioxus_logger::init(Level::INFO).expect("failed to init logger"); - -// let state = AppState::new().await; -// let port = state.env.port; - -// let secure_ip_src = match state.env.production { -// true => SecureClientIpSource::FlyClientIp, -// false => SecureClientIpSource::ConnectInfo, -// }; - -// // Build the routers. -// let built_router = Router::new() -// .route("/", get(serve::serve_built_index)) -// .route("/*file_path", get(serve::serve_other_built)); - -// let shared_router = Router::new() -// .route("/", post(share_project)) -// .route("/:id", get(get_shared_project)); - -// let app = Router::new() -// .route("/ws", get(ws::ws_handler)) -// .nest("/built/:build_id", built_router) -// .nest("/shared", shared_router) -// .route( -// "/", -// get(|| async { Redirect::permanent("https://dioxuslabs.com/play") }), -// ) -// .route("/health", get(|| async { StatusCode::OK })) -// .layer( -// ServiceBuilder::new() -// .layer(HandleErrorLayer::new(|error: BoxError| async move { -// error!(?error, "unhandled server error"); -// (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") -// })) -// .layer(CompressionLayer::new()) -// .layer(CorsLayer::very_permissive()) -// .layer(BufferLayer::new(1024)) -// .layer(RateLimitLayer::new( -// REQUESTS_PER_INTERVAL, -// RATE_LIMIT_INTERVAL, -// )) -// .layer(secure_ip_src.into_extension()) -// .layer(middleware::from_fn_with_state( -// state.clone(), -// request_counter, -// )), -// ) -// .with_state(state); - -// // Start the Axum server. -// let final_address = &format!("0.0.0.0:{port}"); -// let listener = TcpListener::bind(final_address).await.unwrap(); - -// info!("listening on `{}`", final_address); -// axum::serve( -// listener, -// app.into_make_service_with_connect_info::(), -// ) -// .await -// .unwrap(); -// } - -// /// Start misc services for maintaining the server's operation. -// fn start_cleanup_services(state: AppState) { -// tokio::task::spawn(async move { -// let cleanup_delay = state.env.built_cleanup_delay; -// let shutdown_delay = state -// .env -// .shutdown_delay -// .unwrap_or(Duration::from_secs(99999999)); - -// loop { -// let now = Instant::now(); -// let next_shutdown_check = now + shutdown_delay; -// let next_cleanup_check = now + cleanup_delay; - -// select! { -// // Perform the next built project cleanup. -// _ = tokio::time::sleep_until(next_cleanup_check) => { -// if let Err(e) = check_cleanup(state.clone()).await { -// warn!("failed to clean built projects: {e}"); -// } -// } - -// // Check if server should shut down. -// _ = tokio::time::sleep_until(next_shutdown_check), if state.env.shutdown_delay.is_some() => { -// let should_shutdown = check_shutdown(&state, &shutdown_delay).await; -// if should_shutdown { -// // TODO: We could be more graceful here. -// std::process::exit(0); -// } -// } -// } -// } -// }); -// } - -// /// Check and cleanup any expired built projects. -// async fn check_cleanup(state: AppState) -> Result<(), io::Error> { -// let task = tokio::task::spawn_blocking(move || { -// let dir = std::fs::read_dir(state.env.built_path)?; - -// for item in dir { -// let item = item?; -// let path = item.path(); -// let pathname = path.file_name().unwrap().to_string_lossy(); - -// // Always cache the examples - don't remove those. -// if example_projects::get_example_projects() -// .iter() -// .any(|p| p.id().to_string() == pathname) -// { -// continue; -// } - -// let time_elapsed = item -// .metadata() -// .and_then(|m| m.created()) -// .and_then(|c| c.elapsed().map_err(io::Error::other))?; - -// if time_elapsed >= state.env.built_cleanup_delay { -// std::fs::remove_dir_all(path)?; -// } -// } - -// Ok(()) -// }); - -// task.await.expect("task should not panic or abort") -// } - -// /// Check if the server should shutdown. -// async fn check_shutdown(state: &AppState, shutdown_delay: &Duration) -> bool { -// let now = Instant::now(); -// let mut last_req_time = state.last_request_time.lock().await; - -// // Reset timer when build is occuring. -// if state.is_building.load(Ordering::SeqCst) { -// *last_req_time = now; -// return false; -// } - -// // Exit program if not building and duration exceeds shutdown time. -// let duration_since_req = now.duration_since(*last_req_time); -// if duration_since_req.as_secs() >= shutdown_delay.as_secs() { -// return true; -// } - -// false -// } - -// /// A middleware that counts the time since the last request for the shutdown watcher. -// async fn request_counter(State(state): State, req: Request, next: Next) -> Response { -// let now = Instant::now(); -// let mut lock = state.last_request_time.lock().await; -// *lock = now; -// drop(lock); -// next.run(req).await -// } - -fn main() {} +use app::AppState; +use axum::{ + BoxError, Router, + error_handling::HandleErrorLayer, + extract::{Request, State}, + http::StatusCode, + middleware::{self, Next}, + response::{Redirect, Response}, + routing::{get, post}, +}; +use axum_client_ip::ClientIpSource; +use dioxus_logger::tracing::{Level, error, info}; +use share::{get_shared_project, share_project}; +use std::{net::SocketAddr, sync::atomic::Ordering, time::Duration}; +use tokio::{net::TcpListener, time::Instant}; +use tower::{ServiceBuilder, buffer::BufferLayer}; +use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder}; +use tower_http::{compression::CompressionLayer, cors::CorsLayer}; + +mod app; +mod build; +mod built; +mod share; +mod ws; + +#[tokio::main] +async fn main() { + #[cfg(feature = "tracing")] + { + use tracing_subscriber::prelude::*; + + let console_layer = console_subscriber::spawn(); + use tracing_subscriber::prelude::*; + use tracing_subscriber::{EnvFilter, fmt}; + + let fmt_layer = fmt::layer(); + let filter_layer = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + tracing_subscriber::registry() + .with(console_layer) + .with(filter_layer) + .with(fmt_layer) + .init(); + } + _ = dioxus_logger::init(Level::INFO); + + let state = AppState::new().await; + let port = state.env.port; + + let secure_ip_src = match state.env.production { + true => ClientIpSource::FlyClientIp, + false => ClientIpSource::ConnectInfo, + }; + + // Allow bursts with up to 240 requests per IP address + // and replenishes 120 every second. These requests are + // very cheap. Rate limiting for builds is handled seperately + let governor_conf = GovernorConfigBuilder::default() + .per_second(120) + .burst_size(240) + .finish() + .expect("neither per_second nor burst_size are zero"); + + // Build the routers. + let built_router = Router::new() + .route("/", get(built::serve_built_index)) + .route("/{*file_path}", get(built::serve_other_built)); + + let shared_router = Router::new() + .route("/", post(share_project)) + .route("/{:id}", get(get_shared_project)); + + let app = Router::new() + // Every build gets a websocket connection to report build progress. + .route("/ws", get(ws::ws_handler)) + // The built routes for project that are cached. + .nest("/built/{:build_id}", built_router) + // Routes for resolving shared projects. + .nest("/shared", shared_router) + .route( + "/", + get(|| async { Redirect::permanent("https://dioxuslabs.com/play") }), + ) + .route("/health", get(|| async { StatusCode::OK })) + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|error: BoxError| async move { + error!(?error, "unhandled server error"); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error") + })) + .layer(CompressionLayer::new()) + .layer(CorsLayer::very_permissive()) + .layer(BufferLayer::new(1024)) + .layer(GovernorLayer::new(governor_conf)) + .layer(secure_ip_src.into_extension()) + .layer(middleware::from_fn_with_state( + state.clone(), + request_counter, + )), + ) + .with_state(state); + + // Start the Axum server. + let final_address = &format!("0.0.0.0:{port}"); + let listener = TcpListener::bind(final_address).await.unwrap(); + + info!("listening on `{}`", final_address); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); +} + +/// Start misc services for maintaining the server's operation. +fn start_cleanup_services(state: AppState) { + if let Some(shutdown_delay) = state.env.shutdown_delay { + tokio::task::spawn(async move { + loop { + let now = Instant::now(); + let next_shutdown_check = now + shutdown_delay; + + // Check if server should shut down. + tokio::time::sleep_until(next_shutdown_check).await; + let should_shutdown = check_shutdown(&state, &shutdown_delay).await; + if should_shutdown { + // TODO: We could be more graceful here. + std::process::exit(0); + } + } + }); + } +} + +/// Check if the server should shutdown. +async fn check_shutdown(state: &AppState, shutdown_delay: &Duration) -> bool { + let now = Instant::now(); + let mut last_req_time = state.last_request_time.lock().await; + + // Reset timer when build is occuring. + if state.is_building.load(Ordering::SeqCst) { + *last_req_time = now; + return false; + } + + // Exit program if not building and duration exceeds shutdown time. + let duration_since_req = now.duration_since(*last_req_time); + if duration_since_req.as_secs() >= shutdown_delay.as_secs() { + return true; + } + + false +} + +/// A middleware that counts the time since the last request for the shutdown watcher. +async fn request_counter(State(state): State, req: Request, next: Next) -> Response { + let now = Instant::now(); + let mut lock = state.last_request_time.lock().await; + *lock = now; + drop(lock); + next.run(req).await +} diff --git a/packages/playground/server/src/serve.rs b/packages/playground/server/src/serve.rs deleted file mode 100644 index 57c3fba3a4..0000000000 --- a/packages/playground/server/src/serve.rs +++ /dev/null @@ -1,81 +0,0 @@ -use axum::{ - body::Body, - extract::{Path, State}, - http::{header, StatusCode}, - response::IntoResponse, -}; -use dioxus_logger::tracing::warn; -use std::path::PathBuf; -use tokio_util::io::ReaderStream; -use uuid::Uuid; - -use crate::app::AppState; - -/// Handle providing temporary built wasm assets. -/// This should delete temporary projects after 30 seconds. -pub async fn serve_built_index( - State(state): State, - Path(build_id): Path, -) -> impl IntoResponse { - let path = state.env.built_path.join(build_id.to_string()); - - let index_path = path.join("index.html"); - let file = match tokio::fs::File::open(index_path.clone()).await { - Ok(f) => f, - Err(e) => { - warn!(err = ?e, path = ?index_path, "failed to read built project:"); - return Err((StatusCode::NOT_FOUND, "not found")); - } - }; - - let stream = ReaderStream::new(file); - let body = Body::from_stream(stream); - - let headers = [(header::CONTENT_TYPE, "text/html")]; - - Ok((headers, body)) -} - -pub async fn serve_other_built( - State(state): State, - Path((build_id, file_path)): Path<(Uuid, PathBuf)>, -) -> impl IntoResponse { - let path = state - .env - .built_path - .join(build_id.to_string()) - .join(file_path); - - let file = match tokio::fs::File::open(path.clone()).await { - Ok(f) => f, - Err(e) => { - warn!(err = ?e, path = ?path, "failed to read built project:"); - return Err((StatusCode::NOT_FOUND, "read failure")); - } - }; - - let Some(file_ext) = path.extension() else { - warn!(build_id = ?build_id, path = ?path, "failed to get file extension"); - return Err((StatusCode::INTERNAL_SERVER_ERROR, "read failure")); - }; - - let content_type = match file_ext.to_str() { - Some("wasm") => "application/wasm", - Some("js") => "application/javascript", - Some(_) => { - warn!(build_id = ?build_id, path = ?path, "project tried accessing denied file"); - return Err((StatusCode::NOT_FOUND, "not found")); - } - None => { - warn!(build_id = ?build_id, path = ?path, "failed to get file extension"); - return Err((StatusCode::INTERNAL_SERVER_ERROR, "read failure")); - } - }; - - let stream = ReaderStream::new(file); - let body = Body::from_stream(stream); - - let headers = [(header::CONTENT_TYPE, content_type)]; - - Ok((headers, body)) -} diff --git a/packages/playground/server/src/share.rs b/packages/playground/server/src/share.rs index a12554415a..8bd9297dbe 100644 --- a/packages/playground/server/src/share.rs +++ b/packages/playground/server/src/share.rs @@ -1,12 +1,12 @@ use crate::app::AppState; use axum::{ - extract::{Path, State}, Json, + extract::{Path, State}, }; use dioxus_logger::tracing::trace; use gists::{GistFile, NewGist}; -use model::api::{GetSharedProjectRes, ShareProjectReq, ShareProjectRes}; use model::AppError; +use model::api::{GetSharedProjectRes, ShareProjectReq, ShareProjectRes}; use std::collections::HashMap; const PRIMARY_GIST_FILE_NAME: &str = "dxp.rs"; @@ -54,9 +54,10 @@ pub async fn share_project( pub mod gists { use crate::app::AppState; use model::AppError; - use reqwest::{header, StatusCode}; + use reqwest::{StatusCode, header}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; + use uuid::Uuid; const GISTS_URL_PREFIX: &str = "https://api.github.com/gists"; const GITHUB_USER_AGENT: &str = "Dioxus Playground"; @@ -95,7 +96,7 @@ pub mod gists { #[derive(Debug, Serialize, Deserialize)] pub struct Gist { - pub id: String, + pub id: Uuid, pub files: HashMap, } diff --git a/packages/playground/server/src/ws.rs b/packages/playground/server/src/ws.rs index 8de3398610..c5d21e6de0 100644 --- a/packages/playground/server/src/ws.rs +++ b/packages/playground/server/src/ws.rs @@ -1,27 +1,30 @@ +use std::net::IpAddr; + use crate::{ - build::{BuildCommand, BuildMessage, BuildRequest}, AppState, + build::{BuildCommand, BuildMessage, BuildRequest}, }; use axum::{ - extract::{ws::WebSocket, State, WebSocketUpgrade}, + extract::{State, WebSocketUpgrade, ws::WebSocket}, response::IntoResponse, }; -use axum_client_ip::SecureClientIp; +use axum_client_ip::ClientIp; use dioxus_logger::tracing::error; use futures::{SinkExt, StreamExt as _}; +use governor::clock::{Clock, QuantaClock}; use model::{Project, SocketMessage}; use tokio::{ select, sync::mpsc::{self, UnboundedSender}, }; +use uuid::Uuid; /// Handle any pre-websocket processing. pub async fn ws_handler( State(state): State, - SecureClientIp(ip): SecureClientIp, + ClientIp(ip): ClientIp, ws: WebSocketUpgrade, ) -> impl IntoResponse { - let ip = ip.to_string(); ws.on_upgrade(move |socket| handle_socket(state, ip, socket)) } @@ -31,23 +34,9 @@ pub async fn ws_handler( /// - Handle submitting build requests, allowing only one build per socket. /// - Send any build messages to the client. /// - Stop any ongoing builds if the connection closes. -async fn handle_socket(state: AppState, _ip: String, socket: WebSocket) { +async fn handle_socket(state: AppState, ip: IpAddr, socket: WebSocket) { let (mut socket_tx, mut socket_rx) = socket.split(); - // Ensure only one client per socket. - // let mut connected_sockets = state.connected_sockets.lock().await; - // if connected_sockets.contains(&ip) { - // // Client is already connected. Send error and close socket. - // let _ = socket_tx - // .send(SocketMessage::AlreadyConnected.into_axum()) - // .await; - // let _ = socket_tx.close().await; - // return; - // } else { - // connected_sockets.push(ip.clone()); - // } - // drop(connected_sockets); - // Start our build loop. let (build_tx, mut build_rx) = mpsc::unbounded_channel(); let mut current_build: Option = None; @@ -61,17 +50,21 @@ async fn handle_socket(state: AppState, _ip: String, socket: WebSocket) { continue; }; - // Start a new build, stopping any existing ones. - if let SocketMessage::BuildRequest(code) = socket_msg { - if let Some(ref request) = current_build { - let result = state.build_queue_tx.send(BuildCommand::Stop { id: request.id }); - if result.is_err() { - error!(build_id = ?request.id, "failed to send build stop signal for new build request"); - continue; + // Start a new build + if let SocketMessage::BuildRequest { code, previous_build_id } = socket_msg { + // Rate limit the build requests by ip. If we are being rate limited, send a message + // to the client with the wait time and then wait before continuing. + if let Err(n) = state.build_govener.check_key(&ip) { + let wait_time = n.wait_time_from(QuantaClock::default().now()); + let socket_msg = SocketMessage::RateLimited(wait_time); + let socket_result = socket_tx.send(socket_msg.into_axum()).await; + if socket_result.is_err() { + break; } + tokio::time::sleep(wait_time).await; } - let request = start_build(&state, build_tx.clone(), code); + let request = start_build(&state, build_tx.clone(), code, previous_build_id); current_build = Some(request); } } @@ -84,8 +77,8 @@ async fn handle_socket(state: AppState, _ip: String, socket: WebSocket) { break; } - // If the build finished, let's close this socket. - if let BuildMessage::Finished(_) = build_msg { + // If the build finished, close this socket. + if build_msg.is_done() { current_build = None; let _ = socket_tx.close().await; break; @@ -105,14 +98,6 @@ async fn handle_socket(state: AppState, _ip: String, socket: WebSocket) { error!(build_id = ?request.id, "failed to send build stop signal for closed websocket"); } } - - // Drop the socket from our connected list. - // TODO: Convert this to a drop guard. - // let mut connected_sockets = state.connected_sockets.lock().await; - // let index = connected_sockets.iter().position(|x| **x == ip); - // if let Some(index) = index { - // connected_sockets.remove(index); - // } } /// Assembles the build request and sends it to the queue. @@ -120,10 +105,12 @@ fn start_build( state: &AppState, build_tx: UnboundedSender, code: String, + previous_build_id: Option, ) -> BuildRequest { let project = Project::new(code, None, None); let request = BuildRequest { id: project.id(), + previous_build_id, project, ws_msg_tx: build_tx, }; diff --git a/packages/playground/server/template/.gitignore b/packages/playground/server/template/.gitignore index 8f4cc2f477..4f5f03ebd1 100644 --- a/packages/playground/server/template/.gitignore +++ b/packages/playground/server/template/.gitignore @@ -4,4 +4,6 @@ Cargo.lock /Cargo.toml /Dioxus.toml /src/main.rs -.cargo \ No newline at end of file +/src/components +/assets +.cargo diff --git a/packages/playground/server/template/snippets/Cargo.toml b/packages/playground/server/template/snippets/Cargo.toml index b72469b69d..9c65352a44 100644 --- a/packages/playground/server/template/snippets/Cargo.toml +++ b/packages/playground/server/template/snippets/Cargo.toml @@ -4,18 +4,27 @@ [package] name = "play-{BUILD_ID}" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] -dioxus = { version= "0.6", features = ["web", "router"] } +dioxus = { version = "0.7.0-rc.1", features = ["web", "router"] } +dioxus-primitives = { git = "https://github.com/DioxusLabs/components", rev = "3571d90", version = "0.0.1", default-features = false, features = ["router"] } +time = { version = "0.3.44", features = ["std", "macros", "wasm-bindgen"] } +tracing = "0.1.41" +wasm-bindgen = "=0.2.100" +web-sys = { version = "0.3", features = ["Location"] } -[profile.dev] -opt-level = 1 -debug-assertions = true -strip = true +[profile.dev.package."*"] +opt-level = 'z' debug = false -incremental = true +strip = true -[profile.wasm-dev] -inherits = "dev" -opt-level = 1 +[profile.dev.package.dioxus-devtools] +opt-level = 'z' +debug = true +strip = true + +[profile.dev.package.dioxus-web] +opt-level = 'z' +debug = true +strip = true diff --git a/packages/playground/server/template/snippets/Dioxus.toml b/packages/playground/server/template/snippets/Dioxus.toml index 0808933fd0..e06d2f38fb 100644 --- a/packages/playground/server/template/snippets/Dioxus.toml +++ b/packages/playground/server/template/snippets/Dioxus.toml @@ -2,7 +2,7 @@ name = "{BUILD_ID}" default_platform = "web" out_dir = "dist" -asset_dir = "public" +asset_dir = "assets" hot_reload = false [web.app] @@ -20,4 +20,3 @@ script = [] [application.plugins] available = true required = [] - diff --git a/packages/search/search-shared/src/lib.rs b/packages/search/search-shared/src/lib.rs index 4083659761..6e10bdca79 100644 --- a/packages/search/search-shared/src/lib.rs +++ b/packages/search/search-shared/src/lib.rs @@ -326,7 +326,7 @@ impl SearchIndexMapping for BaseDirectoryMapping { fn map_route(&self, route: R) -> Option { let route = route.to_string(); let (route, _) = route.split_once('#').unwrap_or((&route, "")); - let (route, _) = route.split_once('?').unwrap_or((&route, "")); + let (route, _) = route.split_once('?').unwrap_or((route, "")); let route = PathBuf::from(route).join("index.html"); Some(route) }