diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index f7db1c56d..aa7bb2ea0 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 d0eb68ec5..fca61d633 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 25056eaa7..cb7f74d3c 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 f3bb6ed5d..205fc4760 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 10467c595..5f817cc9b 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 6bf9c2370..aaeae8246 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 022e2ba51..75e91c790 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 f4daca6e4..85b34b9a8 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 8d986e258..af25a3cd0 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 ab5c57730..90f82e00f 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 33966cd7d..09e8071f8 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 9ea94d57a..b67b4e8b7 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 6a990854d..2a915a401 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 896ec1206..fd9009aed 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 7e4bd7d69..a1be7153d 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 08964291f..b79ab2ff5 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 b5725c45e..1d1fa7ae1 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 0b11f1a17..49038ae37 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 9fd6f1980..8cfb640d9 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 dcb4a1693..43b160076 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 cbcb9037a..ae5243249 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 047b1fc7c..3a72f0ddd 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 b35e68dfd..10610b122 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 15a95cf47..1b802df4a 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 d48da0aab..37c4d229c 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 47b9bd027..ba6ac1468 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 581e37f01..ad123b44b 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 0ced0a8b6..e13cdd8bd 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 f9a771b6a..809930d4d 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 eead20d54..4c1c4b120 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 c92496cb0..f73311d8a 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 936e6bd41..480bd45f9 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 d5162d9f6..18b636b3f 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 a74288546..32b195b3f 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 000000000..149be2bcc --- /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 2e9b351e2..dd03d7276 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 c24923201..cc1fe0255 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 40ca844d4..d6c5484d2 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 6e571acfb..730536afd 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 bbe63c743..754969335 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 5d154851f..217e05e4e 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 89562b9e4..5130f3c62 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 000000000..7c61d7354 --- /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 8b1a7b0b2..be2302366 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 aaee78892..58562e472 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 cb6d1e684..741f078d8 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 a5feaf574..679eddb87 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 42a1c3755..1aba2f91b 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 a35f7fde6..58e5d6215 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 bf287fca6..3e00f8def 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 000000000..e8b3b5590 --- /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 000000000..f9c44a92c --- /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 000000000..2590c0132 --- /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 000000000..7a21a4efe --- /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 000000000..a7998a2da --- /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 000000000..2590c0132 --- /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 000000000..9ab617697 --- /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 000000000..63fd6097a --- /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 000000000..2590c0132 --- /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 000000000..8884e0d53 --- /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 000000000..0bdf2f2e9 --- /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 000000000..88e70f02d --- /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 000000000..2590c0132 --- /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 000000000..31f0eab44 --- /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 56cb26e07..e5c47fd05 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 4d413c9ce..0fee29739 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 f926a1028..ffc51c589 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 d4b849254..1cc8b7570 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 4192cf4e1..01bf55359 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 000000000..ca0a0e6f8 --- /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 569e8e964..6f5dcfa42 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 1df10b000..7d25883c2 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 f5ea9c420..48c57d829 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 70d455e5e..f40865bd8 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 c1939485d..8c8df1ea9 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 000000000..10e425f16 --- /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 cc04a1422..f8d7dddb7 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 dbad085ee..81d8392d3 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 000000000..0bf1067fc --- /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 d5b2965a3..ba2037636 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 57c3fba3a..000000000 --- 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 a12554415..8bd9297db 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 8de339861..c5d21e6de 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 8f4cc2f47..4f5f03ebd 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 b72469b69..9c65352a4 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 0808933fd..e06d2f38f 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 408365976..6e10bdca7 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) }