From 7ec3453ca36347a22c15634ac60eb6c4127fd57a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Oct 2024 17:23:45 -0700 Subject: [PATCH] Fix bundling/path errors, sidecar support, iOS/Android simulator support, asset hotreload fixes, serverfn hotrelad, native tailwind support (#2779) * wip: fix manganis import path * upgrade tauri bundler, convert todo!() to unimplemented!() * feat: mobile hotreloading + ios asset configuration * remove oid demo and its associated env vars * nuke all the things that cause cache thrashing * swap to Asset type * add some more logs * display impl for attrvalue * dont panic on collect failure * cut down web deps to 150 * clean up deps in a lot of places, simplify build scripts * clean up asset resolution and cli-dev profile * wire up absolute paths for manganis asset in a particular mode * move document related things around to shorten compile times * move most things to `document::Item` and then decompose the cli-config * switch link to stylesheet for clarity in examples * move manganis workspace example to examples folder * decompose manganis-cli-support * dont need mobile demo anymore * remove gloo dialogs * html doesnt need document * rename hotreload to devtools * really clean up the html crate * fix weird rsx spacing * clean up desktop to use callback * clean up document a bit * re-wire up devsocket * fix utf css * yeeeet that js out of here * synthetic web system works * web crate almost done being cleaned up * desktop mostly cleaned up too * clean up cli a bit * more cli cleanup * cli builds again * clean up cli, inline structs, cut down number of unique types where possible * hotreload works again for desktop and this time, mobile too * cfg out tungstenite * devserver err * more refactor to cli builder * switch to unbounded_send * new structure is much cleaner * add http serve subcommand * bundled hotreload * kick stylsheets * clean up a bit more, split up eventloop * amazingly serve seems done * change from pub to pub(crate) in cli * remove tools * bit more polish to cli * fix issue with join * gracefully handle fullstack without a server * fullstack mobile demo * fix launch function, move projects into example-projects folder * hoist examples * add "run" command * clean up launch * remove old manual websocket receiver * doctor command * allow desktop to scroll * cut apart router crate * dont put launch in prelude * use dioxus::launch where possible * rename rsx, cut out hotreload tests * remove liveview project * bump native * fix compile for renderers * move sync event response out of interpreter * move render in serve * rollback settings change * cli compiles, huzzah! * change uris for asset * fix asset * new tui screen * new cargo-like tui works * very very verty close * it works! very small bug with incorrect grapheme calc * Clean up devserver a bit * status sytem * tidy up debug filters * clean up logging situation * Fix a number of bugs with log printing * new printing system is more reliable * wire up more stuff * things working but fullstack is having issues * fullstack works again! * hotreloading bundled assets works again * bundled hotreload and beginnings of macos bundling * Hotreload desktop * combined server builds * add build handle * fix fullstack assets * make open async, add some hooks for ios * migrate filemap to runner * wip global crate system * fixup bundles + organize asset * fix asset location bug * all the bundled reloading! * open ios simulator! * full hotreload support for mobile + serverfn * basic cleanups * clean up dx * Move filemap * fix cutting of newlines * assets workibg, some android * hoist wry/tao * use sync locks and headers to fix issues with android * desktop -> mac/win/linux with alias * better logging * feat: workspace (entire computer!) hotreload * should rebuild toggle, ios simulator bootup * proper mobile support in launch * more robust handling of assets * fix cargo * bring back some of tauri bundle * make warnings go away, clippy happy on cli * some final clippy cleanups * fmt * move manganis to its own folder * upgrade bundle to stable * drastically slim down manganis, prepping for merge * typos, failing test, docsrs config * remove static gen test * nix static gen test * we use --platform web instead of --platform fullstack now * only bind dev urls in desktop/mobile * install gtk * nix static gen * split build dir by app name --- .cargo/config.toml | 6 +- .github/workflows/main.yml | 17 +- Cargo.lock | 5410 +++++++++-------- Cargo.toml | 96 +- examples/all_events.rs | 2 +- examples/calculator.rs | 2 +- examples/calculator_mutable.rs | 51 +- examples/clock.rs | 2 +- examples/control_focus.rs | 2 +- examples/counters.rs | 2 +- examples/crm.rs | 29 +- examples/custom_assets.rs | 15 +- examples/dynamic_asset.rs | 2 +- examples/file_upload.rs | 2 +- examples/flat_router.rs | 2 +- examples/fullstack-auth/Cargo.toml | 1 + examples/fullstack-hello-world/Cargo.toml | 2 +- .../fullstack-hello-world/assets/hello.css | 3 + examples/fullstack-hello-world/src/main.rs | 3 +- examples/fullstack-router/src/main.rs | 2 +- examples/global.rs | 2 +- examples/link.rs | 2 +- examples/meta.rs | 26 +- examples/overlay.rs | 2 +- examples/read_size.rs | 9 +- examples/reducer.rs | 2 +- examples/resize.rs | 5 +- examples/router.rs | 2 +- examples/tailwind/src/main.rs | 11 +- examples/title.rs | 1 - examples/todomvc.rs | 2 +- packages/autofmt/src/collect_macros.rs | 2 +- packages/cli-config/src/lib.rs | 5 - packages/cli/Cargo.toml | 96 +- packages/cli/assets/android/.gitignore | 1 + packages/cli/assets/ios/ios.plist | 25 + packages/cli/assets/macos/mac.plist | 38 + packages/cli/assets/{ => web}/index.html | 0 packages/cli/assets/{ => web}/loading.html | 0 packages/cli/assets/web/loading.js | 0 packages/cli/assets/{ => web}/toast.html | 0 packages/cli/src/assets.rs | 280 +- packages/cli/src/builder/build.rs | 404 ++ packages/cli/src/builder/bundle.rs | 722 +++ packages/cli/src/builder/cargo.rs | 272 - packages/cli/src/builder/fullstack.rs | 128 - packages/cli/src/builder/mod.rs | 282 +- packages/cli/src/builder/prepare_html.rs | 205 - packages/cli/src/builder/progress.rs | 325 +- packages/cli/src/builder/runner.rs | 287 + packages/cli/src/builder/verify.rs | 148 + packages/cli/src/builder/web.rs | 357 +- packages/cli/src/bundle_utils.rs | 196 + packages/cli/src/cli/autoformat.rs | 38 +- packages/cli/src/cli/build.rs | 297 +- packages/cli/src/cli/bundle.rs | 150 +- packages/cli/src/cli/check.rs | 15 +- packages/cli/src/cli/clean.rs | 19 +- packages/cli/src/cli/config.rs | 18 +- packages/cli/src/cli/create.rs | 6 +- packages/cli/src/cli/doctor.rs | 10 + packages/cli/src/cli/init.rs | 6 +- packages/cli/src/cli/link.rs | 127 +- packages/cli/src/cli/mod.rs | 79 +- packages/cli/src/cli/run.rs | 60 + packages/cli/src/cli/serve.rs | 120 +- packages/cli/src/cli/target.rs | 52 + packages/cli/src/cli/translate.rs | 25 +- packages/cli/src/config.rs | 691 +-- packages/cli/src/config/app.rs | 23 + packages/cli/src/config/bundle.rs | 204 + packages/cli/src/config/desktop.rs | 5 + packages/cli/src/config/dioxus_config.rs | 105 + packages/cli/src/config/serve.rs | 34 + packages/cli/src/config/web.rs | 180 + packages/cli/src/dioxus_crate.rs | 838 ++- packages/cli/src/error.rs | 67 +- packages/cli/src/fastfs.rs | 126 + packages/cli/src/filemap.rs | 168 + packages/cli/src/main.rs | 101 +- packages/cli/src/metadata.rs | 4 +- packages/cli/src/platform.rs | 186 + .../src/{serve/logs_tab.rs => profiles.rs} | 0 packages/cli/src/rustup.rs | 170 + packages/cli/src/serve/ansi_buffer.rs | 172 + packages/cli/src/serve/builder.rs | 189 - packages/cli/src/serve/detect.rs | 32 + packages/cli/src/serve/handle.rs | 451 ++ .../cli/src/serve/hot_reloading_file_map.rs | 313 - packages/cli/src/serve/mod.rs | 387 +- packages/cli/src/serve/output.rs | 1568 ++--- packages/cli/src/serve/proxy.rs | 10 +- packages/cli/src/serve/render.rs | 487 -- packages/cli/src/serve/runner.rs | 312 + packages/cli/src/serve/server.rs | 666 +- packages/cli/src/serve/update.rs | 59 + packages/cli/src/serve/watcher.rs | 446 +- packages/cli/src/settings.rs | 42 +- packages/cli/src/tooling.rs | 1 + packages/cli/src/tracer.rs | 276 +- packages/core-macro/src/props/mod.rs | 6 +- packages/core-types/src/attributes.rs | 0 packages/core-types/src/bundled.rs | 16 + packages/core-types/src/event.rs | 203 + packages/core-types/src/formatter.rs | 14 + packages/core-types/src/lib.rs | 4 + packages/core/Cargo.toml | 38 +- packages/core/src/hotreload_utils.rs | 12 +- packages/core/src/lib.rs | 1 + packages/core/src/nodes.rs | 21 +- packages/desktop/Cargo.toml | 7 +- packages/desktop/src/android_sync_lock.rs | 11 + packages/desktop/src/app.rs | 5 +- packages/desktop/src/edits.rs | 24 +- packages/desktop/src/index.html | 2 +- packages/desktop/src/lib.rs | 1 + packages/desktop/src/protocol.rs | 343 +- packages/desktop/src/webview.rs | 45 +- packages/devtools-types/src/lib.rs | 8 +- packages/devtools/src/lib.rs | 11 +- packages/dioxus/Cargo.toml | 2 +- packages/dioxus/src/launch.rs | 26 +- packages/dioxus/src/lib.rs | 2 +- packages/fullstack/Cargo.toml | 2 +- packages/fullstack/src/server/mod.rs | 4 + packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 2 +- packages/interpreter/src/ts/native.ts | 9 + packages/manganis/manganis-core/Cargo.toml | 10 + packages/manganis/manganis-core/src/asset.rs | 85 + packages/manganis/manganis-core/src/lib.rs | 5 + packages/manganis/manganis-core/src/linker.rs | 69 + packages/manganis/manganis-macro/Cargo.toml | 26 + packages/manganis/manganis-macro/README.md | 3 + packages/manganis/manganis-macro/src/asset.rs | 96 + packages/manganis/manganis-macro/src/lib.rs | 19 + .../manganis/manganis-macro/src/linker.rs | 21 + packages/manganis/manganis/Cargo.toml | 41 + packages/manganis/manganis/src/builder.rs | 71 + packages/manganis/manganis/src/folder.rs | 31 + packages/manganis/manganis/src/images.rs | 141 + packages/manganis/manganis/src/lib.rs | 15 + packages/mobile/.cargo/config.toml | 5 +- packages/mobile/.vscode/settings.json | 1 - packages/mobile/Cargo.toml | 8 + packages/mobile/src/lib.rs | 56 + .../playwright-tests/fullstack/src/main.rs | 2 +- .../playwright-tests/playwright.config.js | 17 +- .../static-generation.spec.js | 21 - .../static-generation/.gitignore | 3 - .../static-generation/Cargo.toml | 17 - .../static-generation/src/main.rs | 33 - packages/rsx-hotreload/src/collect.rs | 6 +- packages/rsx-hotreload/src/diff.rs | 6 +- packages/rsx/src/template_body.rs | 17 +- packages/signals/src/global/mod.rs | 80 +- 156 files changed, 11544 insertions(+), 9005 deletions(-) create mode 100644 examples/fullstack-hello-world/assets/hello.css create mode 100644 packages/cli/assets/android/.gitignore create mode 100644 packages/cli/assets/ios/ios.plist create mode 100644 packages/cli/assets/macos/mac.plist rename packages/cli/assets/{ => web}/index.html (100%) rename packages/cli/assets/{ => web}/loading.html (100%) create mode 100644 packages/cli/assets/web/loading.js rename packages/cli/assets/{ => web}/toast.html (100%) create mode 100644 packages/cli/src/builder/build.rs create mode 100644 packages/cli/src/builder/bundle.rs delete mode 100644 packages/cli/src/builder/cargo.rs delete mode 100644 packages/cli/src/builder/fullstack.rs delete mode 100644 packages/cli/src/builder/prepare_html.rs create mode 100644 packages/cli/src/builder/runner.rs create mode 100644 packages/cli/src/builder/verify.rs create mode 100644 packages/cli/src/bundle_utils.rs create mode 100644 packages/cli/src/cli/doctor.rs create mode 100644 packages/cli/src/cli/run.rs create mode 100644 packages/cli/src/cli/target.rs create mode 100644 packages/cli/src/config/app.rs create mode 100644 packages/cli/src/config/bundle.rs create mode 100644 packages/cli/src/config/desktop.rs create mode 100644 packages/cli/src/config/dioxus_config.rs create mode 100644 packages/cli/src/config/serve.rs create mode 100644 packages/cli/src/config/web.rs create mode 100644 packages/cli/src/fastfs.rs create mode 100644 packages/cli/src/filemap.rs create mode 100644 packages/cli/src/platform.rs rename packages/cli/src/{serve/logs_tab.rs => profiles.rs} (100%) create mode 100644 packages/cli/src/rustup.rs create mode 100644 packages/cli/src/serve/ansi_buffer.rs delete mode 100644 packages/cli/src/serve/builder.rs create mode 100644 packages/cli/src/serve/detect.rs create mode 100644 packages/cli/src/serve/handle.rs delete mode 100644 packages/cli/src/serve/hot_reloading_file_map.rs delete mode 100644 packages/cli/src/serve/render.rs create mode 100644 packages/cli/src/serve/runner.rs create mode 100644 packages/cli/src/serve/update.rs create mode 100644 packages/cli/src/tooling.rs create mode 100644 packages/core-types/src/attributes.rs create mode 100644 packages/core-types/src/bundled.rs create mode 100644 packages/core-types/src/event.rs create mode 100644 packages/core-types/src/formatter.rs create mode 100644 packages/desktop/src/android_sync_lock.rs create mode 100644 packages/manganis/manganis-core/Cargo.toml create mode 100644 packages/manganis/manganis-core/src/asset.rs create mode 100644 packages/manganis/manganis-core/src/lib.rs create mode 100644 packages/manganis/manganis-core/src/linker.rs create mode 100644 packages/manganis/manganis-macro/Cargo.toml create mode 100644 packages/manganis/manganis-macro/README.md create mode 100644 packages/manganis/manganis-macro/src/asset.rs create mode 100644 packages/manganis/manganis-macro/src/lib.rs create mode 100644 packages/manganis/manganis-macro/src/linker.rs create mode 100644 packages/manganis/manganis/Cargo.toml create mode 100644 packages/manganis/manganis/src/builder.rs create mode 100644 packages/manganis/manganis/src/folder.rs create mode 100644 packages/manganis/manganis/src/images.rs create mode 100644 packages/manganis/manganis/src/lib.rs delete mode 100644 packages/playwright-tests/static-generation.spec.js delete mode 100644 packages/playwright-tests/static-generation/.gitignore delete mode 100644 packages/playwright-tests/static-generation/Cargo.toml delete mode 100644 packages/playwright-tests/static-generation/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 96d030fa1..1c37730a1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,9 +1,13 @@ [profile] -[profile.dioxus-client] +[profile.dioxus-wasm] inherits = "dev" opt-level = 2 [profile.dioxus-server] inherits = "dev" opt-level = 2 + +[profile.dioxus-android] +inherits = "dev" +opt-level = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b6441cce..a347b1f18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,7 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: "0" RUST_BACKTRACE: 1 - rust_nightly: nightly-2024-07-01 + rust_nightly: nightly-2024-10-20 jobs: check-msrv: @@ -142,8 +142,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: sudo apt-get update + - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev - name: Install Rust ${{ env.rust_nightly }} - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@nightly with: toolchain: ${{ env.rust_nightly }} - uses: Swatinem/rust-cache@v2 @@ -153,11 +155,12 @@ jobs: - name: "doc --lib --all-features" run: | cargo doc --workspace --no-deps --all-features --document-private-items - env: - RUSTFLAGS: --cfg docsrs - RUSTDOCFLAGS: --cfg docsrs - # todo: re-enable warnings - # RUSTDOCFLAGS: --cfg docsrs -Dwarnings + # env: + # RUSTFLAGS: --cfg docsrs + # RUSTDOCFLAGS: --cfg docsrs + # todo: re-enable warnings, private items + # RUSTDOCFLAGS: --cfg docsrs -Dwarnings + # --document-private-items # Check for invalid links in the repository link-check: diff --git a/Cargo.lock b/Cargo.lock index 91625fc10..3e03b19e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -33,12 +23,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aead" version = "0.5.2" @@ -95,7 +79,6 @@ dependencies = [ "const-random", "getrandom 0.2.15", "once_cell", - "serde", "version_check", "zerocopy", ] @@ -178,9 +161,9 @@ dependencies = [ [[package]] name = "ansi-to-tui" -version = "5.0.0-rc.1" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428c2992b874104caf39204b05bf89eab4ceefdd4fcb26caa6759906f547f8e8" +checksum = "cd01ee70df708c9ecb68705af08cee6fe85493992a7e205c52ddbe569314ba47" dependencies = [ "nom", "ratatui", @@ -191,9 +174,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -206,43 +189,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "anymap2" @@ -250,6 +233,170 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "app-store-connect" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fb5489b9bfcfa3aec2f68cc79eafb999b5af9b9d9d70ca8dfe36acdd1b2b05" +dependencies = [ + "anyhow", + "base64 0.21.7", + "clap", + "dirs", + "env_logger 0.10.2", + "jsonwebtoken", + "log", + "pem", + "rand 0.8.5", + "reqwest 0.11.27", + "rsa", + "serde", + "serde_json", + "thiserror", + "x509-certificate", +] + +[[package]] +name = "apple-bundles" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb7c27ee2ca7826adfdc84228cd4c5a84ab57b0a11d269d1d7cd0615238e5a2" +dependencies = [ + "anyhow", + "plist", + "simple-file-manifest", + "walkdir", +] + +[[package]] +name = "apple-codesign" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329820aac7259ca0529d3cc21dd3b4c11651225dfce9e0ce25b121b23f923164" +dependencies = [ + "anyhow", + "app-store-connect", + "apple-bundles", + "apple-flat-package", + "apple-xar", + "aws-config", + "aws-sdk-s3", + "aws-smithy-http", + "aws-smithy-types", + "base64 0.21.7", + "bcder", + "bitflags 2.6.0", + "bytes", + "chrono", + "clap", + "cryptographic-message-syntax", + "der 0.7.9", + "dialoguer", + "difference", + "digest", + "dirs", + "elliptic-curve 0.13.8", + "env_logger 0.10.2", + "figment", + "filetime", + "glob", + "goblin", + "hex", + "log", + "md-5", + "minicbor", + "num-traits", + "object 0.32.2", + "oid-registry", + "once_cell", + "p12", + "p256 0.13.2", + "pem", + "pkcs1", + "pkcs8 0.10.2", + "plist", + "rand 0.8.5", + "rasn", + "rayon", + "regex", + "reqwest 0.11.27", + "ring", + "rsa", + "scroll", + "security-framework", + "security-framework-sys", + "semver", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "signature 2.2.0", + "simple-file-manifest", + "spake2", + "spki 0.7.3", + "subtle", + "tempfile", + "thiserror", + "tokio", + "tungstenite 0.21.0", + "uuid", + "walkdir", + "widestring", + "windows-sys 0.52.0", + "x509", + "x509-certificate", + "xml-rs", + "yasna", + "zeroize", + "zip 0.6.6", + "zip_structs", +] + +[[package]] +name = "apple-flat-package" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6adc520e05304de5ec383487786fa20e9c636fe972e59719cdd93621a2db6f1" +dependencies = [ + "apple-xar", + "cpio-archive", + "flate2", + "scroll", + "serde", + "serde-xml-rs", + "thiserror", +] + +[[package]] +name = "apple-xar" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844e00dc1e665b3cf0bba745aa9c6464292ca512db0c11384511586701eb0335" +dependencies = [ + "base64 0.21.7", + "bcder", + "bzip2", + "chrono", + "cryptographic-message-syntax", + "digest", + "flate2", + "log", + "md-5", + "rand 0.8.5", + "reqwest 0.11.27", + "scroll", + "serde", + "serde-xml-rs", + "sha1", + "sha2", + "signature 2.2.0", + "thiserror", + "url", + "x509-certificate", + "xml-rs", + "xz2", +] + [[package]] name = "ar" version = "0.9.0" @@ -261,6 +408,9 @@ name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc-swap" @@ -276,7 +426,19 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash 0.5.0", ] [[package]] @@ -318,15 +480,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" [[package]] -name = "ast_node" -version = "0.9.9" +name = "asn1-rs" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ "proc-macro2", "quote", - "swc_macros_common", - "syn 2.0.79", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -366,9 +554,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.15" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26a9844c659a2a293d239c7910b752f8487fe122c6c8bd1659bf85a6507c302" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "brotli 7.0.0", "flate2", @@ -465,7 +653,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -531,7 +719,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -548,7 +736,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -583,6 +771,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -600,17 +797,6 @@ dependencies = [ "terminal-prompt", ] -[[package]] -name = "auto_impl" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -633,13 +819,55 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] +[[package]] +name = "aws-config" +version = "1.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6448cfb224dd6a9b9ac734f58622dd0d4751f3589f3b777345745f46b2eb14" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.10.0" @@ -668,6 +896,338 @@ dependencies = [ "paste", ] +[[package]] +name = "aws-runtime" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0656a79cf5e6ab0d4bb2465cd750a7a2fd7ea26c062183ed94225f5782e22365" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http-body 0.4.6", + "lru", + "once_cell", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8776850becacbd3a82a4737a9375ddb5c6832a51379f24443a98e61513f852c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0007b5b8004547133319b6c4e87193eee2a0bcb3e4c18c75d09febe9dab7b383" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fffaa356e7f1c725908b75136d53207fa714e348f365671df14e95a60530ad3" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "p256 0.11.1", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc32c", + "crc32fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.31", + "hyper-rustls 0.24.2", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c9cdc179e6afbf5d391ab08c85eac817b51c87e1892a5edb5f7bbdc64314b4" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa 1.0.11", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.6.20" @@ -681,7 +1241,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "itoa 1.0.11", "matchit", "memchr", @@ -711,7 +1271,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "itoa 1.0.11", "matchit", @@ -783,7 +1343,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "headers 0.4.0", + "headers", "http 1.1.0", "http-body 1.0.1", "http-body-util", @@ -804,7 +1364,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -819,14 +1379,14 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "pin-project-lite", - "rustls 0.23.14", + "rustls 0.23.15", "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower 0.4.13", "tower-service", ] @@ -897,11 +1457,23 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide 0.8.0", - "object", + "object 0.36.5", "rustc-demangle", "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -916,11 +1488,12 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-simd" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" dependencies = [ - "simd-abstraction", + "outref", + "vsimd", ] [[package]] @@ -930,12 +1503,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "better_scoped_tls" -version = "0.1.2" +name = "bcder" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7" +checksum = "c627747a6774aab38beb35990d88309481378558875a41da1a4b2e373c906ef0" dependencies = [ - "scoped-tls", + "bytes", + "smallvec", ] [[package]] @@ -977,7 +1551,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.79", + "syn 2.0.85", "which", ] @@ -993,6 +1567,12 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -1008,6 +1588,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitness" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57792b99d555ebf109c83169228076f7d997e2b37ba1a653850ccd703ac7bab0" +dependencies = [ + "sysctl", + "thiserror", + "uname", + "winapi", +] + [[package]] name = "bitstream-io" version = "2.5.3" @@ -1026,6 +1618,25 @@ dependencies = [ "wyz", ] +[[package]] +name = "bitvec-nom2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" +dependencies = [ + "bitvec", + "nom", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -1041,6 +1652,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "blocking" version = "1.6.1" @@ -1054,6 +1674,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.5.1" @@ -1074,7 +1704,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "syn_derive", ] @@ -1110,24 +1740,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "browserslist-rs" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf0ca73de70c3da94e4194e4a01fe732378f55d47cf4c0588caab22a0dbfa14" -dependencies = [ - "ahash 0.8.11", - "chrono", - "either", - "indexmap 2.6.0", - "itertools 0.13.0", - "nom", - "once_cell", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "bstr" version = "1.10.0" @@ -1139,6 +1751,15 @@ dependencies = [ "serde", ] +[[package]] +name = "buffer-redux" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8acf87c5b9f5897cd3ebb9a327f420e0cae9dd4e5c1d2e36f2c84c571a58f1" +dependencies = [ + "memchr", +] + [[package]] name = "built" version = "0.7.4" @@ -1153,9 +1774,6 @@ name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -dependencies = [ - "allocator-api2", -] [[package]] name = "bytecheck" @@ -1199,9 +1817,25 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "bzip2" @@ -1249,6 +1883,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "camino" version = "1.1.9" @@ -1258,6 +1902,18 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-config2" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1124054becb9262cc15c5e96e82f0d782f2aed3a3034d1f71a6385a6fa9e9595" +dependencies = [ + "home", + "serde", + "serde_derive", + "toml_edit 0.22.22", +] + [[package]] name = "cargo-generate" version = "0.21.3" @@ -1291,12 +1947,12 @@ dependencies = [ "remove_dir_all", "rhai", "sanitize-filename", - "semver 1.0.23", + "semver", "serde", "tempfile", "thiserror", "time", - "toml 0.8.19", + "toml", "walkdir", ] @@ -1317,7 +1973,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver 1.0.23", + "semver", "serde", "serde_json", "thiserror", @@ -1330,7 +1986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" dependencies = [ "serde", - "toml 0.8.19", + "toml", ] [[package]] @@ -1345,6 +2001,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + [[package]] name = "castaway" version = "0.2.3" @@ -1355,10 +2020,19 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.1.30" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -1391,6 +2065,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -1510,7 +2193,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", "terminal_size", ] @@ -1523,7 +2206,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1532,6 +2215,17 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest", +] + [[package]] name = "cmake" version = "0.1.51" @@ -1619,20 +2313,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" - -[[package]] -name = "colored" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" -dependencies = [ - "is-terminal", - "lazy_static", - "winapi", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" @@ -1656,13 +2339,14 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" dependencies = [ "castaway", "cfg-if", "itoa 1.0.11", + "rustversion", "ryu", "static_assertions", ] @@ -1763,26 +2447,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "const-str" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" -dependencies = [ - "const-str-proc-macro", -] - -[[package]] -name = "const-str-proc-macro" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "const_format" version = "0.2.33" @@ -1803,6 +2467,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "const_panic" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1839,6 +2509,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1914,12 +2593,21 @@ dependencies = [ ] [[package]] -name = "core2" +name = "cpio" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +checksum = "80e3adec7390c7643049466136117057188edf5f23efc5c8b4fc8079c8dc34a6" + +[[package]] +name = "cpio-archive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d5133d716d3d82da8c76367ddb0ab1733e2629f1462e4f39947e13b8b4b741" dependencies = [ - "memchr", + "chrono", + "is_executable", + "simple-file-manifest", + "thiserror", ] [[package]] @@ -1946,6 +2634,21 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -2038,16 +2741,16 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "futures-core", - "libc", - "mio 0.8.11", + "mio 1.0.2", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -2068,6 +2771,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -2079,6 +2806,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "cryptographic-message-syntax" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c324ba1028cef7e3a71a00cbf585637bb0215dec2f6a2b566d094190a1309b" +dependencies = [ + "bcder", + "bytes", + "chrono", + "hex", + "pem", + "reqwest 0.11.27", + "ring", + "signature 2.2.0", + "x509-certificate", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -2096,28 +2840,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "cssparser" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa 1.0.11", - "phf 0.11.2", - "smallvec", -] - -[[package]] -name = "cssparser-color" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" -dependencies = [ - "cssparser 0.33.0", -] - [[package]] name = "cssparser-macros" version = "0.6.1" @@ -2125,7 +2847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2135,7 +2857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2157,6 +2879,34 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "cvt" version = "0.1.2" @@ -2168,9 +2918,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4" +checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" dependencies = [ "cc", "cxxbridge-flags", @@ -2180,9 +2930,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1" +checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" dependencies = [ "cc", "codespan-reporting", @@ -2190,24 +2940,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "cxxbridge-flags" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6" +checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff" [[package]] name = "cxxbridge-macro" -version = "1.0.128" +version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60" +checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2230,8 +2980,8 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", - "syn 2.0.79", + "strsim", + "syn 2.0.85", ] [[package]] @@ -2242,15 +2992,9 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn 2.0.85", ] -[[package]] -name = "dary_heap" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" - [[package]] name = "dashmap" version = "5.5.3" @@ -2271,22 +3015,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] -name = "data-url" -version = "0.1.1" +name = "dbl" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" dependencies = [ - "matches", + "generic-array 0.14.7", ] [[package]] -name = "debugid" -version = "0.8.0" +name = "der" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ - "serde", - "uuid", + "const-oid", + "zeroize", ] [[package]] @@ -2310,6 +3054,48 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.85", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -2319,8 +3105,17 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.1", - "syn 2.0.79", + "rustc_version", + "syn 2.0.85", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", ] [[package]] @@ -2342,6 +3137,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "digest" version = "0.10.7" @@ -2397,7 +3198,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2409,7 +3210,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2424,12 +3225,12 @@ dependencies = [ "axum-server", "brotli 6.0.0", "built", + "cargo-config2", "cargo-generate", "cargo_metadata", "cargo_toml", "chrono", "clap", - "colored 2.1.0", "console", "console-subscriber", "crossterm", @@ -2439,28 +3240,30 @@ dependencies = [ "dioxus-cli-config", "dioxus-core", "dioxus-core-types", - "dioxus-devtools", + "dioxus-devtools-types", + "dioxus-fullstack", "dioxus-html", "dioxus-rsx", "dioxus-rsx-hotreload", "dioxus-rsx-rosetta", "dirs", "env_logger 0.11.5", - "fern", "flate2", "fs_extra", "futures-channel", "futures-util", - "headers 0.3.9", + "handlebars", + "headers", "html_parser", - "hyper 1.4.1", - "hyper-rustls", + "hyper 1.5.0", + "hyper-rustls 0.27.3", "hyper-util", "ignore", + "itertools 0.13.0", "krates", - "log", - "manganis-cli-support", + "manganis-core", "notify", + "object 0.36.5", "once_cell", "open", "prettyplease", @@ -2468,30 +3271,34 @@ dependencies = [ "ratatui", "rayon", "regex", - "reqwest", - "rustls 0.23.14", + "reqwest 0.12.8", + "rustls 0.23.15", "serde", "serde_json", - "subprocess", - "syn 2.0.79", + "strum 0.26.3", + "syn 2.0.85", "tar", "tauri-bundler", "tauri-utils", "tempfile", "thiserror", + "throbber-widgets-tui", "tokio", "tokio-stream", - "toml 0.8.19", + "tokio-util", + "toml", "toml_edit 0.22.22", "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", + "unicode-segmentation", + "uuid", "walkdir", "wasm-bindgen-cli-support", "wasm-bindgen-shared", "wasm-opt", - "zip", + "zip 0.6.6", ] [[package]] @@ -2512,16 +3319,16 @@ version = "0.6.0-alpha.3" dependencies = [ "const_format", "dioxus", + "dioxus-core-types", "dioxus-html", "dioxus-ssr", "futures-channel", "futures-util", "generational-box", "longest-increasing-subsequence", - "manganis", "pretty_assertions", "rand 0.8.5", - "reqwest", + "reqwest 0.12.8", "rustc-hash 1.1.0", "rustversion", "serde", @@ -2546,7 +3353,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn 2.0.85", "tokio", "trybuild", ] @@ -2584,8 +3391,9 @@ dependencies = [ "muda", "objc", "objc_id", + "once_cell", "rand 0.8.5", - "reqwest", + "reqwest 0.12.8", "rfd", "rustc-hash 1.1.0", "separator", @@ -2646,10 +3454,10 @@ dependencies = [ [[package]] name = "dioxus-examples" -version = "0.6.0-alpha.2" +version = "0.6.0-alpha.3" dependencies = [ "async-std", - "base64 0.21.7", + "base64 0.22.1", "ciborium", "dioxus", "dioxus-ssr", @@ -2657,14 +3465,12 @@ dependencies = [ "futures-util", "getrandom 0.2.15", "http-range", - "manganis", "rand 0.8.5", - "reqwest", + "reqwest 0.12.8", "separator", "serde", "serde_json", "tokio", - "tracing-subscriber", "web-time", ] @@ -2675,7 +3481,7 @@ dependencies = [ "dioxus-autofmt", "dioxus-rsx-rosetta", "html_parser", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen", ] @@ -2704,12 +3510,12 @@ dependencies = [ "futures-util", "generational-box", "http 1.1.0", - "hyper 1.4.1", - "hyper-rustls", + "hyper 1.5.0", + "hyper-rustls 0.27.3", "once_cell", "parking_lot", "pin-project", - "rustls 0.23.14", + "rustls 0.23.15", "serde", "server_fn", "thiserror", @@ -2734,7 +3540,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "reqwest", + "reqwest 0.12.8", "rustversion", "slab", "tokio", @@ -2779,7 +3585,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "trybuild", ] @@ -2807,7 +3613,7 @@ version = "0.6.0-alpha.3" dependencies = [ "chrono", "http 1.1.0", - "lru 0.12.5", + "lru", "rustc-hash 1.1.0", "thiserror", "tracing", @@ -2871,6 +3677,10 @@ name = "dioxus-mobile" version = "0.6.0-alpha.3" dependencies = [ "dioxus-desktop", + "dioxus-lib", + "jni", + "libc", + "once_cell", ] [[package]] @@ -2892,15 +3702,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "dioxus-playwright-static-generation-test" -version = "0.1.0" -dependencies = [ - "dioxus", - "serde", - "tokio", -] - [[package]] name = "dioxus-playwright-web-test" version = "0.0.1" @@ -2962,7 +3763,7 @@ dependencies = [ "proc-macro2", "quote", "slab", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2974,7 +3775,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2988,7 +3789,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.79", + "syn 2.0.85", "tracing", ] @@ -3005,7 +3806,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3020,7 +3821,7 @@ dependencies = [ "once_cell", "parking_lot", "rand 0.8.5", - "reqwest", + "reqwest 0.12.8", "rustc-hash 1.1.0", "serde", "simple_logger", @@ -3117,7 +3918,7 @@ dependencies = [ "quote", "serde", "server_fn_macro", - "syn 2.0.79", + "syn 2.0.85", "tower-http", ] @@ -3169,6 +3970,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "dissimilar" version = "1.0.9" @@ -3195,7 +4007,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3216,6 +4028,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8 0.10.2", + "rfc6979 0.4.0", + "sha2", + "signature 2.2.0", + "zeroize", +] + [[package]] name = "dtoa" version = "1.0.9" @@ -3237,16 +4065,79 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "eax" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" +dependencies = [ + "aead", + "cipher", + "cmac", + "ctr", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.9", + "digest", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + [[package]] name = "ecommerce-site" version = "0.1.1" dependencies = [ "chrono", "dioxus", - "reqwest", + "reqwest 0.12.8", "serde", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" @@ -3256,6 +4147,47 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff 0.12.1", + "generic-array 0.14.7", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest", + "ff 0.13.0", + "generic-array 0.14.7", + "group 0.13.0", + "hkdf", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -3264,9 +4196,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3277,6 +4209,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum-display-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-primitive-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" +dependencies = [ + "num-traits", + "quote", + "syn 2.0.85", +] + [[package]] name = "enumflags2" version = "0.7.10" @@ -3295,7 +4249,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3316,7 +4270,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3361,6 +4315,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -3447,7 +4411,7 @@ checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3506,15 +4470,31 @@ dependencies = [ ] [[package]] -name = "fern" -version = "0.6.2" +name = "ff" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "colored 1.9.4", - "log", + "rand_core 0.6.4", + "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -3522,7 +4502,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", - "rustc_version 0.4.1", + "rustc_version", +] + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", ] [[package]] @@ -3564,10 +4558,19 @@ dependencies = [ ] [[package]] -name = "flume" -version = "0.11.0" +name = "fluent-uri" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -3613,7 +4616,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3647,17 +4650,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "from_variant" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" -dependencies = [ - "proc-macro2", - "swc_macros_common", - "syn 2.0.79", -] - [[package]] name = "fs-err" version = "2.11.0" @@ -3732,7 +4724,7 @@ version = "0.1.0" dependencies = [ "chrono", "dioxus", - "reqwest", + "reqwest 0.12.8", "serde", "tracing", "tracing-subscriber", @@ -3744,7 +4736,7 @@ name = "fullstack-hello-world-example" version = "0.1.0" dependencies = [ "dioxus", - "reqwest", + "reqwest 0.12.8", "serde", "simple_logger", "tracing", @@ -3874,7 +4866,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4033,6 +5025,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -4198,9 +5191,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.8" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c" +checksum = "f3de3fdca9c75fa4b83a76583d265fa49b1de6b088ebcd210749c24ceeb74660" dependencies = [ "bitflags 2.6.0", "bstr", @@ -4301,9 +5294,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.11" +version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfc4febd088abdcbc9f1246896e57e37b7a34f6909840045a1767c6dafac7af" +checksum = "c04e5a94fdb56b1e91eb7df2658ad16832428b8eeda24ff1a0f0288de2bce554" dependencies = [ "bstr", "gix-trace", @@ -4336,9 +5329,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f" +checksum = "a2007538eda296445c07949cf04f4a767307d887184d6b3e83e2d636533ddc6e" dependencies = [ "bitflags 2.6.0", "gix-path", @@ -4361,15 +5354,15 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b" +checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-utils" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" +checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f" dependencies = [ "fastrand", "unicode-normalization", @@ -4419,7 +5412,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4691,6 +5684,39 @@ dependencies = [ "system-deps", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gtk" version = "0.18.1" @@ -4740,7 +5766,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4793,9 +5819,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.5.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" +checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" dependencies = [ "log", "pest", @@ -4814,15 +5840,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.11", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -4867,21 +5884,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes", - "headers-core 0.2.0", - "http 0.2.12", - "httpdate", - "mime", - "sha1", -] - [[package]] name = "headers" version = "0.4.0" @@ -4890,22 +5892,13 @@ checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", - "headers-core 0.3.0", + "headers-core", "http 1.1.0", "httpdate", "mime", "sha1", ] -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - [[package]] name = "headers-core" version = "0.3.0" @@ -4975,20 +5968,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hstr" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae404c0c5d4e95d4858876ab02eecd6a196bb8caa42050dfa809938833fc412" -dependencies = [ - "hashbrown 0.14.5", - "new_debug_unreachable", - "once_cell", - "phf 0.11.2", - "rustc-hash 1.1.0", - "triomphe", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -5117,9 +6096,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -5141,9 +6120,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -5160,6 +6139,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.31", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.3" @@ -5168,14 +6163,14 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", - "rustls 0.23.14", - "rustls-native-certs", + "rustls 0.23.15", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "webpki-roots 0.26.6", ] @@ -5186,7 +6181,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.30", + "hyper 0.14.31", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -5200,7 +6195,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "native-tls", "tokio", @@ -5219,7 +6214,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2", "tokio", @@ -5256,6 +6251,15 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -5272,12 +6276,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "ignore" version = "0.4.23" @@ -5296,27 +6294,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-traits", - "png", - "qoi", - "tiff", -] - -[[package]] -name = "image" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -5337,32 +6317,19 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error", ] -[[package]] -name = "imagequant" -version = "4.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecc99538c9061ee4d88476f6cd704c9f06575a34f0083affdaa1337a331aa7" -dependencies = [ - "arrayvec", - "once_cell", - "rayon", - "rgb", - "thread_local", -] - [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" @@ -5416,13 +6383,19 @@ dependencies = [ [[package]] name = "infer" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" dependencies = [ "cfb", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inotify" version = "0.9.6" @@ -5449,9 +6422,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array 0.14.7", ] +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn 2.0.85", +] + [[package]] name = "instant" version = "0.1.13" @@ -5479,7 +6463,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -5522,18 +6506,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2069faacbe981460232f880d26bf3c7634e322d49053aa48c27e3ae642f728f1" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "is-terminal" version = "0.4.13" @@ -5561,12 +6533,27 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "iter-read" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" + [[package]] name = "itertools" version = "0.8.2" @@ -5674,9 +6661,6 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -dependencies = [ - "rayon", -] [[package]] name = "js-sys" @@ -5689,24 +6673,71 @@ dependencies = [ [[package]] name = "json-patch" -version = "1.4.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" +checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" dependencies = [ + "jsonptr", "serde", "serde_json", "thiserror", ] [[package]] -name = "jsonc-parser" -version = "0.21.1" +name = "jsonptr" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b56a20e76235284255a09fcd1f45cf55d3c524ea657ebd3854735925c57743d" +checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" dependencies = [ + "fluent-uri", + "serde", "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "jzon" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ab85f84ca42c5ec520e6f3c9966ba1fd62909ce260f8837e248857d2560509" + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2", + "signature 2.2.0", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -5718,6 +6749,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -5747,7 +6798,7 @@ dependencies = [ "camino", "cfg-expr 0.17.0", "petgraph", - "semver 1.0.23", + "semver", "serde", "serde_json", ] @@ -5768,7 +6819,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ - "cssparser 0.27.2", + "cssparser", "html5ever", "indexmap 1.9.3", "matches", @@ -5817,33 +6868,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" - -[[package]] -name = "libflate" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" -dependencies = [ - "adler32", - "core2", - "crc32fast", - "dary_heap", - "libflate_lz77", -] - -[[package]] -name = "libflate_lz77" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" -dependencies = [ - "core2", - "hashbrown 0.14.5", - "rle-decode-fast", -] +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libfuzzer-sys" @@ -5953,50 +6980,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "lightningcss" -version = "1.0.0-alpha.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e225b3fa0a8bd5562c8833b1a32afa88761c4e661d3177b8cdc4e13cbf078e" -dependencies = [ - "ahash 0.8.11", - "bitflags 2.6.0", - "const-str", - "cssparser 0.33.0", - "cssparser-color", - "dashmap", - "data-encoding", - "getrandom 0.2.15", - "itertools 0.10.5", - "lazy_static", - "lightningcss-derive", - "parcel_selectors", - "parcel_sourcemap", - "paste", - "pathdiff", - "rayon", - "serde", - "smallvec", -] - -[[package]] -name = "lightningcss-derive" -version = "1.0.0-alpha.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252" -dependencies = [ - "convert_case 0.6.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "line-col" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e69cdf6b85b5c8dce514f694089a2cf8b1a702f6cd28607bcb3cf296c9778db" - [[package]] name = "link-cplusplus" version = "1.0.9" @@ -6051,7 +7034,7 @@ checksum = "3b51f1d220e3fa869e24cfd75915efe3164bd09bb11b3165db3f37f57bf673e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6079,6 +7062,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -6103,15 +7092,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "lru" version = "0.12.5" @@ -6121,6 +7101,17 @@ dependencies = [ "hashbrown 0.15.0", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -6148,118 +7139,35 @@ dependencies = [ [[package]] name = "manganis" -version = "0.3.0-alpha.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3857dfc4889510de7db73105aff3906d2e9ee3c30823d9014d8c236e2c7a92c0" +version = "0.6.0-alpha.3" dependencies = [ + "anyhow", + "base64 0.22.1", + "dioxus-core-types", + "dunce", "manganis-macro", + "once_cell", + "serde", ] [[package]] -name = "manganis-cli-support" -version = "0.3.0-alpha.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a63972fc54ec8162f3d18d95b2dd7fcf2becb127b0cb3fed318fc4be46c3eeda" +name = "manganis-core" +version = "0.6.0-alpha.3" dependencies = [ - "anyhow", - "image 0.25.2", - "imagequant", - "lightningcss", - "manganis-common", - "mozjpeg", - "object", - "png", - "railwind", - "ravif", - "rayon", - "reqwest", - "rustc-hash 1.1.0", "serde", "serde_json", - "swc", - "swc_allocator", - "swc_atoms", - "swc_cached", - "swc_common", - "swc_compiler_base 0.19.0", - "swc_config", - "swc_config_macro", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_codegen_macros", - "swc_ecma_compat_bugfixes 0.12.0", - "swc_ecma_compat_common", - "swc_ecma_compat_es2015 0.12.0", - "swc_ecma_compat_es2016 0.12.0", - "swc_ecma_compat_es2017 0.12.0", - "swc_ecma_compat_es2018 0.12.0", - "swc_ecma_compat_es2019 0.12.0", - "swc_ecma_compat_es2020 0.12.0", - "swc_ecma_compat_es2021 0.12.0", - "swc_ecma_compat_es2022 0.12.0", - "swc_ecma_compat_es3 0.12.0", - "swc_ecma_ext_transforms", - "swc_ecma_lints 0.100.0", - "swc_ecma_loader", - "swc_ecma_minifier 0.204.0", - "swc_ecma_parser", - "swc_ecma_preset_env 0.217.0", - "swc_ecma_transforms 0.239.0", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_classes 0.134.0", - "swc_ecma_transforms_compat 0.171.0", - "swc_ecma_transforms_macros", - "swc_ecma_transforms_module 0.190.0", - "swc_ecma_transforms_optimization 0.208.0", - "swc_ecma_transforms_proposal 0.178.0", - "swc_ecma_transforms_react 0.191.0", - "swc_ecma_transforms_typescript 0.198.1", - "swc_ecma_usage_analyzer", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_eq_ignore_macros", - "swc_error_reporters", - "swc_fast_graph", - "swc_macros_common", - "swc_node_comments", - "swc_timer", - "swc_trace_macro", - "swc_transform_common", - "swc_typescript", - "swc_visit", - "tracing", - "url", -] - -[[package]] -name = "manganis-common" -version = "0.3.0-alpha.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77bbcc6cf753925e251a2c5a59fdf2da3214975185226601465792ca2e34ea98" -dependencies = [ - "anyhow", - "base64 0.21.7", - "built", - "home", - "infer 0.11.0", - "serde", - "toml 0.7.8", - "tracing", - "url", ] [[package]] name = "manganis-macro" -version = "0.3.0-alpha.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f20a6bb40193a285313478a5db79589f7dd8926c3dc662a9f114cc5690e556d" +version = "0.6.0-alpha.3" dependencies = [ - "manganis-common", + "manganis-core", "proc-macro2", "quote", + "serde", "serde_json", - "syn 2.0.79", - "tracing-subscriber", + "syn 2.0.85", ] [[package]] @@ -6346,31 +7254,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "miette" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" -dependencies = [ - "cfg-if", - "miette-derive", - "owo-colors", - "textwrap", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "mime" version = "0.3.17" @@ -6388,10 +7271,30 @@ dependencies = [ ] [[package]] -name = "minicov" -version = "0.3.5" +name = "minicbor" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +checksum = "9d15f4203d71fdf90903c2696e55426ac97a363c67b218488a73b534ce7aca10" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "minicov" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def6d99771d7c499c26ad4d40eb6645eafd3a1553b35fc26ea5a489a45e82d9a" dependencies = [ "cc", "walkdir", @@ -6442,6 +7345,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -6452,31 +7356,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" -[[package]] -name = "mozjpeg" -version = "0.10.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969e1dbc9af2f18ffe6ddba72bbe86506c7214ecb28670d98ecfba51cb9b8c6b" -dependencies = [ - "arrayvec", - "bytemuck", - "libc", - "mozjpeg-sys", - "rgb", -] - -[[package]] -name = "mozjpeg-sys" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e31c0171e0b1158c0dfb7386dbdf999f4a9afaa83fd68de39c7929f4d5c16f" -dependencies = [ - "cc", - "dunce", - "libc", - "nasm-rs", -] - [[package]] name = "muda" version = "0.11.5" @@ -6521,15 +7400,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "nasm-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" -dependencies = [ - "jobserver", -] - [[package]] name = "native-tls" version = "0.2.12" @@ -6547,20 +7417,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ndk" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" -dependencies = [ - "bitflags 1.3.2", - "jni-sys", - "ndk-sys 0.4.1+23.1.7779620", - "num_enum 0.5.11", - "raw-window-handle 0.5.2", - "thiserror", -] - [[package]] name = "ndk" version = "0.9.0" @@ -6570,8 +7426,8 @@ dependencies = [ "bitflags 2.6.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", - "num_enum 0.7.3", + "ndk-sys", + "num_enum", "raw-window-handle 0.6.2", "thiserror", ] @@ -6582,15 +7438,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.4.1+23.1.7779620" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -6669,15 +7516,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "normpath" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a9da8c9922c35a1033d76f7272dfc2e7ee20392083d75aeea6ced23c6266578" -dependencies = [ - "winapi", -] - [[package]] name = "normpath" version = "1.3.0" @@ -6717,6 +7555,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -6725,7 +7577,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "serde", ] [[package]] @@ -6741,10 +7592,20 @@ dependencies = [ "num-iter", "num-traits", "rand 0.8.5", + "serde", "smallvec", "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -6759,7 +7620,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6803,44 +7664,13 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive 0.5.11", -] - [[package]] name = "num_enum" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive 0.7.3", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", + "num_enum_derive", ] [[package]] @@ -6852,7 +7682,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6909,6 +7739,20 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.14.5", + "indexmap 2.6.0", + "memchr", + "ruzstd 0.5.0", +] + [[package]] name = "object" version = "0.36.5" @@ -6917,10 +7761,41 @@ checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "flate2", "memchr", - "ruzstd", + "ruzstd 0.7.2", "wasmparser 0.218.0", ] +[[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once-cell-regex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de7e389a5043420c8f2b95ed03f3f104ad6f4c41f7d7e27298f033abc253e8" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -6952,9 +7827,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -6973,7 +7848,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6984,9 +7859,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -7022,9 +7897,9 @@ dependencies = [ [[package]] name = "outref" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" [[package]] name = "overload" @@ -7042,6 +7917,72 @@ dependencies = [ "supports-color 3.0.1", ] +[[package]] +name = "p12" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224" +dependencies = [ + "cbc", + "cipher", + "des", + "getrandom 0.2.15", + "hmac", + "lazy_static", + "rc2", + "sha1", + "yasna", +] + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct 0.2.0", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", + "rand_core 0.6.4", + "sha2", +] + [[package]] name = "pango" version = "0.18.3" @@ -7067,36 +8008,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "parcel_selectors" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4d26c18a8377a64728c04bf3b2e48ec43b0c77e687a18e03eb837d65e08a14" -dependencies = [ - "bitflags 2.6.0", - "cssparser 0.33.0", - "fxhash", - "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "precomputed-hash", - "smallvec", -] - -[[package]] -name = "parcel_sourcemap" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb" -dependencies = [ - "base64-simd", - "data-url", - "rkyv", - "serde", - "serde_json", - "vlq", -] - [[package]] name = "parking" version = "2.2.1" @@ -7137,6 +8048,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -7152,18 +8074,6 @@ dependencies = [ "path-dedot", ] -[[package]] -name = "path-clean" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" - -[[package]] -name = "path-clean" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" - [[package]] name = "path-dedot" version = "3.1.1" @@ -7187,10 +8097,43 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", - "password-hash", + "password-hash 0.4.2", "sha2", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -7237,7 +8180,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7261,6 +8204,70 @@ dependencies = [ "indexmap 2.6.0", ] +[[package]] +name = "pgp" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6c842436d5fa2b59eac1e9b3d142b50bfff99c1744c816b1f4c2ac55a20754" +dependencies = [ + "aes", + "aes-gcm", + "argon2", + "base64 0.22.1", + "bitfield", + "block-padding", + "blowfish", + "bstr", + "buffer-redux", + "byteorder", + "camellia", + "cast5", + "cfb-mode", + "chrono", + "cipher", + "const-oid", + "crc24", + "curve25519-dalek", + "derive_builder", + "des", + "digest", + "dsa", + "eax", + "ecdsa 0.16.9", + "ed25519-dalek", + "elliptic-curve 0.13.8", + "flate2", + "generic-array 0.14.7", + "hex", + "hkdf", + "idea", + "iter-read", + "k256", + "log", + "md-5", + "nom", + "num-bigint-dig", + "num-traits", + "num_enum", + "ocb3", + "p256 0.13.2", + "p384", + "p521", + "rand 0.8.5", + "ripemd", + "rsa", + "sha1", + "sha1-checked", + "sha2", + "sha3", + "signature 2.2.0", + "smallvec", + "thiserror", + "twofish", + "x25519-dalek", + "zeroize", +] + [[package]] name = "phf" version = "0.8.0" @@ -7365,7 +8372,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7397,29 +8404,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -7444,9 +8451,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.9", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -7455,8 +8472,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -7465,6 +8482,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.7.0" @@ -7579,24 +8602,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "preset_env_base" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b30eab18be480c194938e433e269d5298a279f6410f02fbc73f3576a325c110" -dependencies = [ - "ahash 0.8.11", - "anyhow", - "browserslist-rs", - "dashmap", - "from_variant", - "once_cell", - "semver 1.0.23", - "serde", - "st-map", - "tracing", -] - [[package]] name = "prettier-please" version = "0.3.0" @@ -7604,7 +8609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32db37eb2b0ec0af154e9c1b33425902d8cd9481e35167c4e9ffb28fec3916bb" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7629,12 +8634,21 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.85", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", ] [[package]] @@ -7697,9 +8711,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -7712,8 +8726,9 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "version_check", + "yansi", ] [[package]] @@ -7738,7 +8753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7761,7 +8776,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7773,15 +8788,6 @@ dependencies = [ "prost", ] -[[package]] -name = "psm" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" -dependencies = [ - "cc", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -7837,7 +8843,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.14", + "rustls 0.23.15", "socket2", "thiserror", "tokio", @@ -7854,7 +8860,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.14", + "rustls 0.23.15", "slab", "thiserror", "tinyvec", @@ -7889,28 +8895,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_fmt" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" - -[[package]] -name = "railwind" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc2b6ca634162c78a6e8d399e12e22b42bcbf2eb60f2b7a2f99aa2379d41800" -dependencies = [ - "hex", - "indexmap 1.9.3", - "lazy_static", - "line-col", - "regex", - "ron", - "serde", - "serde_regex", -] - [[package]] name = "rand" version = "0.7.3" @@ -7993,19 +8977,57 @@ dependencies = [ ] [[package]] -name = "ratatui" -version = "0.27.0" +name = "rasn" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +checksum = "cf9b0d03fbc7d2dcfdd35086c43ce30ac5ff62ed7eff4397e4f4f2995a2b0e2a" +dependencies = [ + "arrayvec", + "bitvec", + "bitvec-nom2", + "bytes", + "chrono", + "either", + "jzon", + "konst", + "nom", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rasn-derive", + "snafu", +] + +[[package]] +name = "rasn-derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbaf7105cd254b632f4732fbcc243ce750cef87d8335826125ef6df5733b5a0c" +dependencies = [ + "either", + "itertools 0.10.5", + "proc-macro2", + "quote", + "rayon", + "syn 1.0.109", + "uuid", +] + +[[package]] +name = "ratatui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", + "instability", "itertools 0.13.0", - "lru 0.12.5", + "lru", "paste", - "stability", "strum 0.26.3", "strum_macros 0.26.4", "unicode-segmentation", @@ -8050,9 +9072,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", @@ -8094,6 +9116,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -8146,6 +9177,12 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -8168,7 +9205,7 @@ dependencies = [ "cvt", "fs_at", "libc", - "normpath 1.3.0", + "normpath", "windows-sys 0.59.0", ] @@ -8181,6 +9218,48 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.31", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + [[package]] name = "reqwest" version = "0.12.8" @@ -8197,8 +9276,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", - "hyper-rustls", + "hyper 1.5.0", + "hyper-rustls 0.27.3", "hyper-tls", "hyper-util", "ipnet", @@ -8211,17 +9290,17 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.14", + "rustls 0.23.15", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-util", "tower-service", "url", @@ -8230,7 +9309,28 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots 0.26.6", - "windows-registry", + "windows-registry 0.2.0", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", ] [[package]] @@ -8261,9 +9361,6 @@ name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] [[package]] name = "rhai" @@ -8290,7 +9387,7 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -8308,6 +9405,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -8337,25 +9443,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rle-decode-fast" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.6.0", - "indexmap 2.6.0", - "serde", - "serde_derive", -] - [[package]] name = "router-static-generation" version = "0.1.0" @@ -8364,6 +9451,36 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rpm" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c49b6baaa0e8fa864b2069d1e94e7a132471da3ac26a132f3fa7e71416772c" +dependencies = [ + "bitflags 2.6.0", + "bzip2", + "chrono", + "cpio", + "digest", + "enum-display-derive", + "enum-primitive-derive", + "flate2", + "hex", + "itertools 0.13.0", + "log", + "md-5", + "nom", + "num", + "num-derive", + "num-traits", + "pgp", + "sha1", + "sha2", + "thiserror", + "xz2", + "zstd 0.13.2", +] + [[package]] name = "rsa" version = "0.9.6" @@ -8376,10 +9493,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -8418,22 +9535,22 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.23", + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", ] [[package]] @@ -8455,6 +9572,7 @@ version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ + "log", "ring", "rustls-webpki 0.101.7", "sct", @@ -8462,9 +9580,23 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.14" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" dependencies = [ "aws-lc-rs", "log", @@ -8476,6 +9608,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-native-certs" version = "0.8.0" @@ -8509,9 +9666,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -8541,6 +9698,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "ruzstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +dependencies = [ + "byteorder", + "derive_more", + "twox-hash", +] + [[package]] name = "ruzstd" version = "0.7.2" @@ -8556,12 +9724,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "ryu-js" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" - [[package]] name = "same-file" version = "1.0.6" @@ -8608,6 +9770,26 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "sct" version = "0.7.1" @@ -8624,6 +9806,34 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array 0.14.7", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.9", + "generic-array 0.14.7", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -8654,7 +9864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ "bitflags 1.3.2", - "cssparser 0.27.2", + "cssparser", "derive_more", "fxhash", "log", @@ -8667,15 +9877,6 @@ dependencies = [ "thin-slice", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.23" @@ -8685,12 +9886,6 @@ dependencies = [ "serde", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "send_wrapper" version = "0.6.0" @@ -8708,13 +9903,24 @@ checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + [[package]] name = "serde-wasm-bindgen" version = "0.5.0" @@ -8727,21 +9933,33 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.210" +name = "serde-xml-rs" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa 1.0.11", "memchr", @@ -8770,16 +9988,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "serde_regex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" -dependencies = [ - "regex", - "serde", -] - [[package]] name = "serde_repr" version = "0.1.19" @@ -8788,7 +9996,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -8839,7 +10047,20 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa 1.0.11", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] @@ -8856,11 +10077,11 @@ dependencies = [ "gloo-net 0.6.0", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "inventory", "js-sys", "once_cell", - "reqwest", + "reqwest 0.12.8", "send_wrapper", "serde", "serde_json", @@ -8887,7 +10108,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "xxhash-rust", ] @@ -8898,7 +10119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" dependencies = [ "server_fn_macro", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -8922,6 +10143,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -8939,6 +10170,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -8977,7 +10218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", + "mio 1.0.2", "signal-hook", ] @@ -8992,21 +10233,22 @@ dependencies = [ [[package]] name = "signature" -version = "2.2.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest", "rand_core 0.6.4", ] [[package]] -name = "simd-abstraction" -version = "0.7.1" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "outref", + "digest", + "rand_core 0.6.4", ] [[package]] @@ -9030,6 +10272,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple-file-manifest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74" + [[package]] name = "simple-static-generation" version = "0.1.0" @@ -9038,13 +10286,25 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "simple_logger" version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" dependencies = [ - "colored 2.1.0", + "colored", "log", "time", "windows-sys 0.48.0", @@ -9082,7 +10342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33a1b4f13e2bbf2f5b29d09dfebc9de69229ffee245aed80e3b70f9b5fd28c06" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -9122,10 +10382,27 @@ dependencies = [ ] [[package]] -name = "smawk" -version = "0.3.2" +name = "snafu" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "socket2" @@ -9137,6 +10414,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "soup3" version = "0.5.0" @@ -9164,22 +10452,15 @@ dependencies = [ ] [[package]] -name = "sourcemap" -version = "9.0.0" +name = "spake2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab08a862c70980b8e23698b507e272317ae52a608a164a844111f5372374f1f" +checksum = "c5482afe85a0b6ce956c945401598dbc527593c77ba51d0a87a586938b1b893a" dependencies = [ - "base64-simd", - "bitvec", - "data-encoding", - "debugid", - "if_chain", - "rustc-hash 1.1.0", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id-start", - "url", + "curve25519-dalek", + "hkdf", + "rand_core 0.6.4", + "sha2", ] [[package]] @@ -9191,6 +10472,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -9198,7 +10489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -9434,56 +10725,12 @@ dependencies = [ "uuid", ] -[[package]] -name = "st-map" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8257dd592de7614be71a2342d36ba2d527ddad3f9a0c8d09d6ceed4c371531e4" -dependencies = [ - "arrayvec", - "static-map-macro", -] - -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn 2.0.79", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "static-map-macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e9696ef338691287aeb937ee6ffe60022f579d3c8d2fd9d58973a9a10a466" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -9516,18 +10763,6 @@ dependencies = [ "quote", ] -[[package]] -name = "string_enum" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.79", -] - [[package]] name = "stringprep" version = "0.1.5" @@ -9539,12 +10774,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -9589,17 +10818,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", -] - -[[package]] -name = "subprocess" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" -dependencies = [ - "libc", - "winapi", + "syn 2.0.85", ] [[package]] @@ -9636,1430 +10855,6 @@ dependencies = [ "serde", ] -[[package]] -name = "swc" -version = "0.283.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb7fe4bd6a604528819c84fc9acc5d5ec1b3bd0d11d13ee5d8a77d49ff678fa" -dependencies = [ - "anyhow", - "base64 0.21.7", - "dashmap", - "either", - "indexmap 2.6.0", - "jsonc-parser", - "lru 0.10.1", - "once_cell", - "parking_lot", - "pathdiff", - "regex", - "rustc-hash 1.1.0", - "serde", - "serde_json", - "sourcemap", - "swc_atoms", - "swc_cached", - "swc_common", - "swc_compiler_base 0.16.0", - "swc_config", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_ext_transforms", - "swc_ecma_lints 0.99.0", - "swc_ecma_loader", - "swc_ecma_minifier 0.201.0", - "swc_ecma_parser", - "swc_ecma_preset_env 0.214.0", - "swc_ecma_transforms 0.236.0", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_compat 0.170.0", - "swc_ecma_transforms_optimization 0.205.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_error_reporters", - "swc_node_comments", - "swc_timer", - "swc_transform_common", - "swc_typescript", - "swc_visit", - "tracing", - "url", -] - -[[package]] -name = "swc_allocator" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc8bd3075d1c6964010333fae9ddcd91ad422a4f8eb8b3206a9b2b6afb4209e" -dependencies = [ - "bumpalo", - "hashbrown 0.14.5", - "ptr_meta", - "rustc-hash 1.1.0", - "triomphe", -] - -[[package]] -name = "swc_atoms" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125" -dependencies = [ - "hstr", - "once_cell", - "rustc-hash 1.1.0", - "serde", -] - -[[package]] -name = "swc_cached" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83406221c501860fce9c27444f44125eafe9e598b8b81be7563d7036784cd05c" -dependencies = [ - "ahash 0.8.11", - "anyhow", - "dashmap", - "once_cell", - "regex", - "serde", -] - -[[package]] -name = "swc_common" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d0a8eaaf1606c9207077d75828008cb2dfb51b095a766bd2b72ef893576e31" -dependencies = [ - "ahash 0.8.11", - "ast_node", - "better_scoped_tls", - "cfg-if", - "either", - "from_variant", - "new_debug_unreachable", - "num-bigint", - "once_cell", - "parking_lot", - "rustc-hash 1.1.0", - "serde", - "siphasher", - "sourcemap", - "swc_allocator", - "swc_atoms", - "swc_eq_ignore_macros", - "swc_visit", - "tracing", - "unicode-width", - "url", -] - -[[package]] -name = "swc_compiler_base" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b47fac27c2e84e033bdea294ac12f575322c81c9d5d44f423e3f11ff239a86" -dependencies = [ - "anyhow", - "base64 0.21.7", - "once_cell", - "pathdiff", - "rustc-hash 1.1.0", - "serde", - "serde_json", - "sourcemap", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_minifier 0.201.0", - "swc_ecma_parser", - "swc_ecma_visit", - "swc_timer", -] - -[[package]] -name = "swc_compiler_base" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb87f8dc7be1a034d5c29bcc4be9d504ddfd2f8aa1a1338fc568e104e087d29" -dependencies = [ - "anyhow", - "base64 0.21.7", - "once_cell", - "pathdiff", - "rustc-hash 1.1.0", - "serde", - "serde_json", - "sourcemap", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_minifier 0.204.0", - "swc_ecma_parser", - "swc_ecma_visit", - "swc_timer", -] - -[[package]] -name = "swc_config" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" -dependencies = [ - "anyhow", - "indexmap 2.6.0", - "serde", - "serde_json", - "sourcemap", - "swc_cached", - "swc_config_macro", -] - -[[package]] -name = "swc_config_macro" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f56139042c1a95b54f5ca48baa0e0172d369bcc9d3d473dad1de36bae8399" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.79", -] - -[[package]] -name = "swc_ecma_ast" -version = "0.118.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f866d12e4d519052b92a0a86d1ac7ff17570da1272ca0c89b3d6f802cd79df" -dependencies = [ - "bitflags 2.6.0", - "is-macro", - "num-bigint", - "phf 0.11.2", - "scoped-tls", - "serde", - "string_enum", - "swc_atoms", - "swc_common", - "unicode-id-start", -] - -[[package]] -name = "swc_ecma_codegen" -version = "0.155.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7641608ef117cfbef9581a99d02059b522fcca75e5244fa0cbbd8606689c6f" -dependencies = [ - "memchr", - "num-bigint", - "once_cell", - "serde", - "sourcemap", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_codegen_macros", - "tracing", -] - -[[package]] -name = "swc_ecma_codegen_macros" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859fabde36db38634f3fad548dd5e3410c1aebba1b67a3c63e67018fa57a0bca" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.79", -] - -[[package]] -name = "swc_ecma_compat_bugfixes" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f9cac39f19d6509db921f4b75934aaa64fc84b599416e5c1fcaed1c313132f" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_es2015 0.11.1", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_bugfixes" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75429b44cc479cbe018d5994eddae5ac7ab887ebefeb3596720921bc4cdff551" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_es2015 0.12.0", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_common" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9acdf402b36f8e83084b10e119d7ba9d07e5229ef39e1343f147db816c7b73e" -dependencies = [ - "swc_common", - "swc_ecma_ast", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", -] - -[[package]] -name = "swc_ecma_compat_es2015" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade6e0c6e8ddb61281fee2331c3775a920c31535b91e8cace2e0c4eed6158d3" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "is-macro", - "serde", - "serde_derive", - "smallvec", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_classes 0.133.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2015" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c988d9018d6abb22b0fcc2da6a624be2db7c56681b6180d1cb5faa2672fd8001" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "is-macro", - "rustc-hash 1.1.0", - "serde", - "serde_derive", - "smallvec", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_classes 0.134.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2016" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209e347cdc3fb56632a1d882f981f3448f5f529c16d8da9d770207fffda4a8f6" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2016" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7a3e086151c70ff940531ddcd04c01351ae80aa4593fd2906255d18a836b4f" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2017" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa87a6861b2adc8b0178fb450165101c4396409481c8726ec90ad28398cae5d" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2017" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b74c89c9bd4fa532fba3d1ec47b129ec450b4143d3914118cd61b0e44d4a4b" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2018" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f577f098e7c3738ade709caadb17c9f3bd911ea2ee6cfacca561d12addcc5761" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2018" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40bf74a06c433eee502ea6347596d5766d77da8baf32653d14a6655df4e181a" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2019" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d52253dc2f83a3fca526c387c33e4ff9a8423b68c271414c9f870e1ced3231" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2019" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10afb20890ffda37eefdfe06c3bb0d12e5ec8698667cb9e3689b74066b398845" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2020" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed343932876fad34b1d4a13e30c55b94531e89916f45e7c04203bc49a29565b9" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_es2022 0.11.0", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2020" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0608c4814a362d5362bc536507d8c89b287521778e8b678fe4590bfa1843803a" -dependencies = [ - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_es2022 0.12.0", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2021" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6b28a2c109466eaa809d9b9a5b81dcbb4e269ba293a9c5c34aabc67b6427bc" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2021" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f12ffb0f4282f4b333efa98c9653d181d89e1b5339d4be0d789189a246ef34b" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2022" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3a644e271ea2a9df88e3e456c5c204c4916ef5136b7d946f9cd25607f47ec6" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_classes 0.133.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es2022" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc16be9dd64e1b32569375b0b73ecc7dc74f9d848e8caaf2007896e2cf8d68a7" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_compat_common", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_classes 0.134.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es3" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55ffadc12067b21524bf7b5d6938021ee918f65f18937ed27245c23544bc910" -dependencies = [ - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_compat_es3" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e684ae87d26ad3012e588d0e268158cadee10ddc0cda261069f0f280a8b23ce7" -dependencies = [ - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_ext_transforms" -version = "0.120.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad03ee53c734eb74757d03c07ec71b1a982261830c9253ef3e2e4a089f9af25d" -dependencies = [ - "phf 0.11.2", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_lints" -version = "0.99.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c11bcc9e3dc49929500c07c8b0c84a88064847d31e9ee16204b257e6bd315c" -dependencies = [ - "auto_impl", - "dashmap", - "parking_lot", - "rayon", - "regex", - "serde", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_lints" -version = "0.100.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89907376ce67b56d8fbf79cca830a12cb41f93dccc306008c07d8eba8f6d388e" -dependencies = [ - "auto_impl", - "dashmap", - "parking_lot", - "rayon", - "regex", - "serde", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_loader" -version = "0.49.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55fa3d55045b97894bfb04d38aff6d6302ac8a6a38e3bb3dfb0d20475c4974a9" -dependencies = [ - "anyhow", - "dashmap", - "lru 0.10.1", - "normpath 0.2.0", - "once_cell", - "parking_lot", - "path-clean 0.1.0", - "pathdiff", - "serde", - "serde_json", - "swc_atoms", - "swc_cached", - "swc_common", - "tracing", -] - -[[package]] -name = "swc_ecma_minifier" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d75a42254926bad8b7fa9390767a18ac608d99cfd5a3be675e4739c2cf7db1b" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "num-bigint", - "num_cpus", - "once_cell", - "parking_lot", - "phf 0.11.2", - "radix_fmt", - "regex", - "rustc-hash 1.1.0", - "ryu-js", - "serde", - "serde_json", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_optimization 0.205.0", - "swc_ecma_usage_analyzer", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_timer", - "tracing", -] - -[[package]] -name = "swc_ecma_minifier" -version = "0.204.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d88917a66b8f457c5953d2ff2d7788259658c89578636b28e9ac6ae56bbfd9" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "num-bigint", - "num_cpus", - "once_cell", - "parking_lot", - "phf 0.11.2", - "radix_fmt", - "regex", - "rustc-hash 1.1.0", - "ryu-js", - "serde", - "serde_json", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_optimization 0.208.0", - "swc_ecma_usage_analyzer", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_timer", - "tracing", -] - -[[package]] -name = "swc_ecma_parser" -version = "0.149.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683dada14722714588b56481399c699378b35b2ba4deb5c4db2fb627a97fb54b" -dependencies = [ - "either", - "new_debug_unreachable", - "num-bigint", - "num-traits", - "phf 0.11.2", - "serde", - "smallvec", - "smartstring", - "stacker", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "tracing", - "typed-arena", -] - -[[package]] -name = "swc_ecma_preset_env" -version = "0.214.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85162c77f8c80b55e5aed3a5e5477b74a53ce9cca05dec6e3dabec1de0f49af" -dependencies = [ - "anyhow", - "dashmap", - "indexmap 2.6.0", - "once_cell", - "preset_env_base", - "rustc-hash 1.1.0", - "semver 1.0.23", - "serde", - "serde_json", - "st-map", - "string_enum", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms 0.236.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_preset_env" -version = "0.217.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51992e6bb854ef2e6c7a1b9a14ed8d0e3c8f905d348f694759f9a97bfa6a425" -dependencies = [ - "anyhow", - "dashmap", - "indexmap 2.6.0", - "once_cell", - "preset_env_base", - "rustc-hash 1.1.0", - "semver 1.0.23", - "serde", - "serde_json", - "st-map", - "string_enum", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms 0.239.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms" -version = "0.236.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d289a83ab829d076d6c2bf2cc0beea7fbf0480ac47a415401f683181a65f4856" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_compat 0.170.0", - "swc_ecma_transforms_module 0.187.0", - "swc_ecma_transforms_optimization 0.205.0", - "swc_ecma_transforms_proposal 0.178.0", - "swc_ecma_transforms_react 0.190.0", - "swc_ecma_transforms_typescript 0.195.1", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82df2dd8048fe23f1df72acd52bfebf846b3d5a76e048eee32acf9af9bee6a98" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_compat 0.171.0", - "swc_ecma_transforms_proposal 0.179.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_base" -version = "0.144.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c0a71579d030e12fd3cfbfc8712c4ce21afc526f2a759903c77d8df61950f5e" -dependencies = [ - "better_scoped_tls", - "bitflags 2.6.0", - "indexmap 2.6.0", - "once_cell", - "phf 0.11.2", - "rustc-hash 1.1.0", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_utils", - "swc_ecma_visit", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_base" -version = "0.145.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" -dependencies = [ - "better_scoped_tls", - "bitflags 2.6.0", - "indexmap 2.6.0", - "once_cell", - "phf 0.11.2", - "rustc-hash 1.1.0", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_utils", - "swc_ecma_visit", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_classes" -version = "0.133.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f37ec04525798a09ce02e52dc15433acee2d86664da0b8ede55bb5cefd95384" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_classes" -version = "0.134.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d884594385bea9405a2e1721151470d9a14d3ceec5dd773c0ca6894791601" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_compat" -version = "0.170.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb500b65423646da940e289ad37e7c88332d7194248c33fc63a9e768e104fe5" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "is-macro", - "num-bigint", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_compat_bugfixes 0.11.0", - "swc_ecma_compat_common", - "swc_ecma_compat_es2015 0.11.1", - "swc_ecma_compat_es2016 0.11.0", - "swc_ecma_compat_es2017 0.11.1", - "swc_ecma_compat_es2018 0.11.0", - "swc_ecma_compat_es2019 0.11.0", - "swc_ecma_compat_es2020 0.11.0", - "swc_ecma_compat_es2021 0.11.0", - "swc_ecma_compat_es2022 0.11.0", - "swc_ecma_compat_es3 0.11.0", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_classes 0.133.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_compat" -version = "0.171.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f23da29c1279b6e0c1ac0df9d0f7fd6c955a141a9770e5a0a2d54292509bcf6" -dependencies = [ - "arrayvec", - "indexmap 2.6.0", - "is-macro", - "num-bigint", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_compat_bugfixes 0.12.0", - "swc_ecma_compat_common", - "swc_ecma_compat_es2015 0.12.0", - "swc_ecma_compat_es2016 0.12.0", - "swc_ecma_compat_es2017 0.12.0", - "swc_ecma_compat_es2018 0.12.0", - "swc_ecma_compat_es2019 0.12.0", - "swc_ecma_compat_es2020 0.12.0", - "swc_ecma_compat_es2021 0.12.0", - "swc_ecma_compat_es2022 0.12.0", - "swc_ecma_compat_es3 0.12.0", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_classes 0.134.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_trace_macro", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_macros" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500a1dadad1e0e41e417d633b3d6d5de677c9e0d3159b94ba3348436cdb15aab" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.79", -] - -[[package]] -name = "swc_ecma_transforms_module" -version = "0.187.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc022be297cdc70d5e71720587c7e810401a958577d77830f3108411e73c466d" -dependencies = [ - "Inflector", - "anyhow", - "bitflags 2.6.0", - "indexmap 2.6.0", - "is-macro", - "path-clean 1.0.1", - "pathdiff", - "regex", - "serde", - "swc_atoms", - "swc_cached", - "swc_common", - "swc_ecma_ast", - "swc_ecma_loader", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_utils", - "swc_ecma_visit", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_module" -version = "0.190.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4d0255362149854b923125e9910ce0a5405ce6d03fb325c5fdd8e9f13a0845" -dependencies = [ - "Inflector", - "anyhow", - "bitflags 2.6.0", - "indexmap 2.6.0", - "is-macro", - "path-clean 1.0.1", - "pathdiff", - "regex", - "serde", - "swc_atoms", - "swc_cached", - "swc_common", - "swc_ecma_ast", - "swc_ecma_loader", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_utils", - "swc_ecma_visit", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_optimization" -version = "0.205.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17446e46b75654901d962251ec4d0063423ee81759a325ed82fcf073308d97ca" -dependencies = [ - "dashmap", - "indexmap 2.6.0", - "once_cell", - "petgraph", - "rustc-hash 1.1.0", - "serde_json", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_fast_graph", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_optimization" -version = "0.208.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d8447ea20ef76958a8240feef95743702485a84331e6df5bdbe7e383c87838" -dependencies = [ - "dashmap", - "indexmap 2.6.0", - "once_cell", - "petgraph", - "rustc-hash 1.1.0", - "serde_json", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_fast_graph", - "tracing", -] - -[[package]] -name = "swc_ecma_transforms_proposal" -version = "0.178.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7ddb3aae86f19eb9e41b0c62509d8e400c1dc79c0889df98f6df1ab893f3f" -dependencies = [ - "either", - "rustc-hash 1.1.0", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_classes 0.133.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_proposal" -version = "0.179.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79938ff510fc647febd8c6c3ef4143d099fdad87a223680e632623d056dae2dd" -dependencies = [ - "either", - "rustc-hash 1.1.0", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_classes 0.134.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_react" -version = "0.190.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e54a8c87d90812bf69b0f07931bb629111a3f24efe83b9190c3a40a5ebc25e" -dependencies = [ - "base64 0.21.7", - "dashmap", - "indexmap 2.6.0", - "once_cell", - "serde", - "sha1", - "string_enum", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_react" -version = "0.191.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" -dependencies = [ - "base64 0.21.7", - "dashmap", - "indexmap 2.6.0", - "once_cell", - "serde", - "sha1", - "string_enum", - "swc_allocator", - "swc_atoms", - "swc_common", - "swc_config", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_macros", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_typescript" -version = "0.195.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f814b32ec83fde097df19e7346c429825390d156d0015f321f1f6434b6a06c0c" -dependencies = [ - "ryu-js", - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.144.0", - "swc_ecma_transforms_react 0.190.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_transforms_typescript" -version = "0.198.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15455da4768f97186c40523e83600495210c11825d3a44db43383fd81eace88d" -dependencies = [ - "ryu-js", - "serde", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_transforms_base 0.145.0", - "swc_ecma_transforms_react 0.191.0", - "swc_ecma_utils", - "swc_ecma_visit", -] - -[[package]] -name = "swc_ecma_usage_analyzer" -version = "0.30.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7689421c6a892642c5907fd608c56d982fdef0d6456f9dba3cc418c6ea7e07" -dependencies = [ - "indexmap 2.6.0", - "rustc-hash 1.1.0", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_utils", - "swc_ecma_visit", - "swc_timer", - "tracing", -] - -[[package]] -name = "swc_ecma_utils" -version = "0.134.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" -dependencies = [ - "indexmap 2.6.0", - "num_cpus", - "once_cell", - "rustc-hash 1.1.0", - "ryu-js", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_visit", - "tracing", - "unicode-id", -] - -[[package]] -name = "swc_ecma_visit" -version = "0.104.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1c6802e68e51f336e8bc9644e9ff9da75d7da9c1a6247d532f2e908aa33e81" -dependencies = [ - "new_debug_unreachable", - "num-bigint", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_visit", - "tracing", -] - -[[package]] -name = "swc_eq_ignore_macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "swc_error_reporters" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d049e9256abf29d9fc66d3db3ea44b6815a64ad565ce31e117a74ee96478bb3" -dependencies = [ - "anyhow", - "miette", - "once_cell", - "parking_lot", - "swc_common", -] - -[[package]] -name = "swc_fast_graph" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357e2c97bb51431d65080f25b436bc4e2fc1a7f64a643bc21a8353e478dc799f" -dependencies = [ - "indexmap 2.6.0", - "petgraph", - "rustc-hash 1.1.0", - "swc_common", -] - -[[package]] -name = "swc_macros_common" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "swc_node_comments" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d016ab18b432523b2a3c104ce3aaf7d869db46c0a41477dbfb6201ddc86c1eb0" -dependencies = [ - "dashmap", - "swc_atoms", - "swc_common", -] - -[[package]] -name = "swc_timer" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5fb6f8b8b85512aacbb3d7140a828666e0e0b1bcc69bf84000a0cd36306bab" -dependencies = [ - "tracing", -] - -[[package]] -name = "swc_trace_macro" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9719b6085dd2824fd61938a881937be14b08f95e2d27c64c825a9f65e052ba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "swc_transform_common" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda3e80e1ad638d3575bc07745a914af13dcb02215098659f864731078271f2c" -dependencies = [ - "better_scoped_tls", - "once_cell", - "rustc-hash 1.1.0", - "serde", - "serde_json", -] - -[[package]] -name = "swc_typescript" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d043347b109a8aebfe01aaeada4af322304ea0f54ae8e5721df9afcb9305ca" -dependencies = [ - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "thiserror", -] - -[[package]] -name = "swc_visit" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceb044142ba2719ef9eb3b6b454fce61ab849eb696c34d190f04651955c613d" -dependencies = [ - "either", - "new_debug_unreachable", -] - [[package]] name = "syn" version = "1.0.109" @@ -11073,9 +10868,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -11091,7 +10886,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -11109,6 +10904,42 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "sysctl" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e483f02d0ad107168dc57381a8a40c3aeea6abe47f37506931f861643cfa8" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "libc", + "thiserror", + "walkdir", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -11117,7 +10948,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -11139,23 +10980,24 @@ dependencies = [ "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", - "toml 0.8.19", + "toml", "version-compare", ] [[package]] name = "tao" -version = "0.26.2" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ebbccb78deb5a36744c079eea2981b4a48ecbbe6b1b2ffbaa528bea3f5e5db" +checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751" dependencies = [ - "bitflags 1.3.2", - "cocoa 0.25.0", - "core-foundation 0.9.4", - "core-graphics 0.23.2", + "bitflags 2.6.0", + "cocoa 0.26.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", + "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", @@ -11164,9 +11006,9 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk 0.7.0", + "ndk", "ndk-context", - "ndk-sys 0.4.1+23.1.7779620", + "ndk-sys", "objc", "once_cell", "parking_lot", @@ -11176,7 +11018,8 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.54.0", + "windows", + "windows-core 0.58.0", "windows-version", "x11-dl", ] @@ -11189,7 +11032,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -11216,44 +11059,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] -name = "tauri-bundler" -version = "1.4.8" +name = "target-triple" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4693930d30c4836ef41f51097502fd77a0ae9f54c3af6aafb8997b2a81f40b4" +checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" + +[[package]] +name = "tauri-bundler" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e04e7ac7c62bc6f65665326ec61e81b9379cec3da5fb6ced5b076145fab15b5" dependencies = [ "anyhow", "ar", - "dirs-next", + "bitness", + "dirs", "dunce", + "flate2", "glob", "handlebars", - "heck 0.4.1", + "heck 0.5.0", "hex", - "image 0.24.9", - "libflate", + "image", "log", "md5", "os_pipe", "plist", "regex", - "semver 1.0.23", + "rpm", + "semver", "serde", "serde_json", "sha1", "sha2", - "strsim 0.10.0", + "strsim", "tar", "tauri-icns", + "tauri-macos-sign", "tauri-utils", "tempfile", "thiserror", "time", "ureq", + "url", "uuid", "walkdir", - "windows-sys 0.48.0", - "winreg", - "zip", + "windows-registry 0.3.0", + "windows-sys 0.59.0", + "zip 2.2.0", ] [[package]] @@ -11267,30 +11120,55 @@ dependencies = [ ] [[package]] -name = "tauri-utils" -version = "1.5.4" +name = "tauri-macos-sign" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "450b17a7102e5d46d4bdabae0d1590fd27953e704e691fc081f06c06d2253b35" +checksum = "be95e2d37d1e3605831da071234418c786684af48982509af1b7f50b31af4546" +dependencies = [ + "anyhow", + "apple-codesign", + "chrono", + "dirs-next", + "log", + "once-cell-regex", + "os_pipe", + "p12", + "plist", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "x509-certificate", +] + +[[package]] +name = "tauri-utils" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc65d6f5c54e56b66258948a6d9e47a82ea41f4b5a7612bfbdd1634c2913ed0" dependencies = [ "ctor", "dunce", "glob", - "heck 0.5.0", "html5ever", - "infer 0.13.0", + "infer 0.16.0", "json-patch", "kuchikiki", "log", "memchr", "phf 0.11.2", - "semver 1.0.23", + "regex", + "semver", "serde", + "serde-untagged", "serde_json", "serde_with", "thiserror", + "toml", "url", + "urlpattern", + "uuid", "walkdir", - "windows-version", ] [[package]] @@ -11345,17 +11223,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - [[package]] name = "thin-slice" version = "0.1.1" @@ -11370,22 +11237,22 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -11398,6 +11265,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "throbber-widgets-tui" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46994acd1137e3e5b78d9b9112d91596b20e64fc6ec788dfc0f2430a2e965633" +dependencies = [ + "rand 0.8.5", + "ratatui", +] + [[package]] name = "tiff" version = "0.9.1" @@ -11478,9 +11355,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -11513,7 +11390,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -11526,13 +11403,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.14", + "rustls 0.23.15", "rustls-pki-types", "tokio", ] @@ -11558,7 +11445,7 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.14", + "rustls 0.23.15", "tokio", "tokio-native-tls", "tungstenite 0.23.0", @@ -11584,25 +11471,15 @@ checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "futures-util", "hashbrown 0.14.5", "pin-project-lite", + "slab", "tokio", ] -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", -] - [[package]] name = "toml" version = "0.8.19" @@ -11632,8 +11509,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.6.0", - "serde", - "serde_spanned", "toml_datetime", "winnow 0.5.40", ] @@ -11676,7 +11551,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-timeout", "percent-encoding", "pin-project", @@ -11788,7 +11663,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -11862,16 +11737,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "triomphe" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" -dependencies = [ - "serde", - "stable_deref_trait", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -11880,17 +11745,40 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207aa50d36c4be8d8c6ea829478be44a372c6a77669937bb39c698e52f1491e8" +checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" dependencies = [ "dissimilar", "glob", "serde", "serde_derive", "serde_json", + "target-triple", "termcolor", - "toml 0.8.19", + "toml", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", ] [[package]] @@ -11930,6 +11818,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -11941,10 +11838,10 @@ dependencies = [ ] [[package]] -name = "typed-arena" -version = "2.0.2" +name = "typeid" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" [[package]] name = "typenum" @@ -11952,6 +11849,21 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -11970,14 +11882,70 @@ dependencies = [ ] [[package]] -name = "unicase" -version = "2.7.0" +name = "uname" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" dependencies = [ "version_check", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -11990,30 +11958,12 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" -[[package]] -name = "unicode-id" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" - -[[package]] -name = "unicode-id-start" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e2a3c5fc9de285c0e805d98eba666adb4b2d9e1049ce44821ff7707cc34e91" - [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-normalization" version = "0.1.24" @@ -12074,6 +12024,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" @@ -12095,8 +12051,9 @@ dependencies = [ "base64 0.22.1", "log", "once_cell", - "rustls 0.23.14", + "rustls 0.23.15", "rustls-pki-types", + "socks", "url", "webpki-roots 0.26.6", ] @@ -12119,6 +12076,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -12133,9 +12102,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -12161,9 +12130,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" [[package]] name = "vcpkg" @@ -12184,10 +12153,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "vlq" -version = "0.5.1" +name = "vsimd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "walkdir" @@ -12224,7 +12193,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12254,7 +12223,7 @@ checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12297,7 +12266,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -12365,7 +12334,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -12410,7 +12379,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12511,9 +12480,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -12532,7 +12501,7 @@ dependencies = [ "bitflags 2.6.0", "hashbrown 0.14.5", "indexmap 2.6.0", - "semver 1.0.23", + "semver", "serde", ] @@ -12649,10 +12618,10 @@ checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.58.0", + "windows", "windows-core 0.58.0", - "windows-implement 0.58.0", - "windows-interface 0.58.0", + "windows-implement", + "windows-interface", ] [[package]] @@ -12663,7 +12632,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12673,7 +12642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" dependencies = [ "thiserror", - "windows 0.58.0", + "windows", "windows-core 0.58.0", ] @@ -12705,6 +12674,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "wifi-scanner" version = "0.1.1" @@ -12759,18 +12734,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-implement 0.53.0", - "windows-interface 0.53.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -12790,40 +12753,19 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings", + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] -[[package]] -name = "windows-implement" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -12832,18 +12774,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", -] - -[[package]] -name = "windows-interface" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12854,7 +12785,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -12863,17 +12794,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result 0.2.0", - "windows-strings", + "windows-result", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] [[package]] -name = "windows-result" -version = "0.1.2" +name = "windows-registry" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a" dependencies = [ + "windows-result", + "windows-strings 0.2.0", "windows-targets 0.52.6", ] @@ -12892,7 +12825,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result 0.2.0", + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663" +dependencies = [ "windows-targets 0.52.6", ] @@ -13139,9 +13081,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.51.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -13149,9 +13091,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.43.1" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728" +checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" dependencies = [ "base64 0.22.1", "block", @@ -13168,7 +13110,7 @@ dependencies = [ "jni", "kuchikiki", "libc", - "ndk 0.9.0", + "ndk", "objc", "objc_id", "once_cell", @@ -13181,7 +13123,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.58.0", + "windows", "windows-core 0.58.0", "windows-version", "x11-dl", @@ -13217,6 +13159,47 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3cec94c3999f31341553f358ef55f65fc031291a022cd42ec0ce7219560c76" +dependencies = [ + "chrono", + "cookie-factory", +] + +[[package]] +name = "x509-certificate" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der 0.7.9", + "hex", + "pem", + "ring", + "signature 2.2.0", + "spki 0.7.3", + "thiserror", + "zeroize", +] + [[package]] name = "xattr" version = "1.3.1" @@ -13238,18 +13221,45 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xxhash-rust" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" + [[package]] name = "zbus" version = "4.4.0" @@ -13292,7 +13302,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "zvariant_utils", ] @@ -13325,7 +13335,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -13333,6 +13343,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] [[package]] name = "zip" @@ -13354,6 +13378,48 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.6.0", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zip_structs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce824a6bfffe8942820fa36d24973b7c83a40896749a42e33de0abdd11750ee5" +dependencies = [ + "byteorder", + "bytesize", + "thiserror", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -13448,7 +13514,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "zvariant_utils", ] @@ -13460,5 +13526,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index 38fbe68fc..6618fe6b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,26 @@ [workspace] resolver = "2" members = [ + "packages/dioxus", + "packages/dioxus-lib", + "packages/core", + "packages/core-types", + "packages/cli", + "packages/core-types", + "packages/core-macro", + "packages/config-macro", + "packages/router-macro", + "packages/extension", + "packages/router", + "packages/html", + "packages/html-internal-macro", + "packages/hooks", + "packages/web", + "packages/ssr", + "packages/desktop", + "packages/mobile", + "packages/interpreter", + "packages/liveview", "packages/autofmt", "packages/check", "packages/cli-config", @@ -34,7 +54,30 @@ members = [ "packages/signals", "packages/ssr", "packages/static-generation", - "packages/web", + "packages/lazy-js-bundle", + "packages/cli-config", + "packages/devtools", + "packages/devtools-types", + "packages/isrg", + "packages/rsx-hotreload", + + # Static generation examples + # "packages/static-generation/examples/simple", + # "packages/static-generation/examples/router", + # "packages/static-generation/examples/github-pages", + + # Playwright tests + "packages/playwright-tests/liveview", + "packages/playwright-tests/web", + "packages/playwright-tests/fullstack", + "packages/playwright-tests/suspense-carousel", + "packages/playwright-tests/nested-suspense", + + # manganis + "packages/manganis/manganis", + "packages/manganis/manganis-macro", + "packages/manganis/manganis-core", + # Full project examples "example-projects/fullstack-hackernews", @@ -57,12 +100,10 @@ members = [ # Playwright tests "packages/playwright-tests/liveview", "packages/playwright-tests/web", - "packages/playwright-tests/static-generation", "packages/playwright-tests/fullstack", "packages/playwright-tests/suspense-carousel", "packages/playwright-tests/nested-suspense", ] -exclude = ["examples/mobile_demo", "examples/openid_connect_demo"] [workspace.package] version = "0.6.0-alpha.3" @@ -75,16 +116,15 @@ dioxus-core = { path = "packages/core", version = "0.6.0-alpha.3" } dioxus-core-types = { path = "packages/core-types", version = "0.6.0-alpha.3" } dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.3" } dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.3" } +dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3" } dioxus-router = { path = "packages/router", version = "0.6.0-alpha.3" } dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.3" } -dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3", default-features = false } dioxus-html = { path = "packages/html", version = "0.6.0-alpha.3", default-features = false } dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.3" } dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.3" } dioxus-web = { path = "packages/web", version = "0.6.0-alpha.3", default-features = false } -dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" } dioxus-ssr = { path = "packages/ssr", version = "0.6.0-alpha.3", default-features = false } -dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3", default-features = false } +dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3" } dioxus-mobile = { path = "packages/mobile", version = "0.6.0-alpha.3" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.0-alpha.3" } dioxus-liveview = { path = "packages/liveview", version = "0.6.0-alpha.3" } @@ -94,23 +134,26 @@ dioxus-rsx = { path = "packages/rsx", version = "0.6.0-alpha.3" } dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.0-alpha.3" } dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.0-alpha.3" } dioxus-signals = { path = "packages/signals", version = "0.6.0-alpha.3" } -dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" } -generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" } dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" } dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" } -dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.3" } +dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.3", default-features = false } dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" } dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false } +dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" } lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" } +dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" } +generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" } + +manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" } +manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" } +manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" } -manganis-cli-support = { version = "0.3.0-alpha.3", features = ["html"] } -manganis = { version = "0.3.0-alpha.3", default-features = false, features = ["html", "macro"]} warnings = { version = "0.2.0" } -# a fork of pretty please for tests +# a fork of pretty please for tests - let's get off of this if we can! prettier-please = { version = "0.3.0", features = ["verbatim"]} - +clap = { version = "4.5.7" } askama_escape = "0.10.3" tracing = "0.1.37" tracing-futures = "0.2.5" @@ -128,8 +171,8 @@ thiserror = "1.0.40" prettyplease = { version = "0.2.20", features = ["verbatim"] } const_format = "0.2.32" cargo_toml = { version = "0.20.3" } -tauri-utils = { version = "=1.5.*" } -tauri-bundler = { version = "=1.4.*" } +tauri-utils = { version = "=2.0.2" } +tauri-bundler = { version = "=2.0.4" } lru = "0.12.2" async-trait = "0.1.77" axum = "0.7.0" @@ -178,7 +221,7 @@ criterion = { version = "0.5" } walrus = "*" # desktop -wry = { version = "0.43.0", default-features = false } +wry = { version = "0.45.0", default-features = false } tao = { version = "0.30.0", features = ["rwh_05"] } webbrowser = "1.0.1" infer = "0.16.0" @@ -192,13 +235,11 @@ core-foundation = "0.10.0" objc = { version = "0.2.7", features = ["exception"] } objc_id = "0.1.1" -[profile.dev.package.dioxus-core-macro] -opt-level = 3 - -# wasm bindgen is slooooooow, but it's because we're actually processing the wasm -# so, lets just bump up walrus to make it faster, no need for any special profiles -[profile.dev.package.walrus] -opt-level = 3 +# our release profile should be fast to compile and fast to run +# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental +[profile.release] +incremental = true +debug = 0 # Disable debug assertions to check the released path of core and other packages, but build without optimizations to keep build times quick [profile.release-unoptimized] @@ -219,15 +260,13 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] rust-version = "1.79.0" publish = false -version = "0.6.0-alpha.2" +version = "0.6.0-alpha.3" [dependencies] -manganis = { workspace = true, optional = true } reqwest = { workspace = true, features = ["json"], optional = true } +ciborium = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } http-range = { version = "0.1.5", optional = true } -ciborium = { version = "0.2.1", optional = true } -base64 = { version = "0.21.0", optional = true } -tracing-subscriber = "0.3.17" [dev-dependencies] dioxus = { workspace = true, features = ["router"] } @@ -264,7 +303,6 @@ fullstack = ["dioxus/fullstack"] axum = ["dioxus/axum"] server = ["dioxus/axum"] web = ["dioxus/web"] -collect-assets = ["dep:manganis"] http = ["dep:reqwest", "dep:http-range"] [[example]] diff --git a/examples/all_events.rs b/examples/all_events.rs index 0e572c32f..ab4620d3a 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -6,7 +6,7 @@ use dioxus::prelude::*; use std::{collections::VecDeque, fmt::Debug, rc::Rc}; -const STYLE: &str = asset!("./examples/assets/events.css"); +const STYLE: Asset = asset!("/examples/assets/events.css"); fn main() { dioxus::launch(app); diff --git a/examples/calculator.rs b/examples/calculator.rs index 6056e1810..c82517d57 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -12,7 +12,7 @@ use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/calculator.css"); +const STYLE: Asset = asset!("/examples/assets/calculator.css"); fn main() { dioxus::LaunchBuilder::desktop() diff --git a/examples/calculator_mutable.rs b/examples/calculator_mutable.rs index e5ca06c85..35dfed3f5 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -29,7 +29,10 @@ fn app() -> Element { let mut state = use_signal(Calculator::new); rsx! { - document::Link { rel: "stylesheet", href: asset!("./examples/assets/calculator.css") } + document::Link { + rel: "stylesheet", + href: asset!("./examples/assets/calculator.css"), + } div { id: "wrapper", div { class: "app", div { @@ -39,15 +42,37 @@ fn app() -> Element { div { class: "calculator-keypad", div { class: "input-keys", div { class: "function-keys", - CalculatorKey { name: "key-clear", onclick: move |_| state.write().clear_display(), - if state.read().display_value == "0" { "C" } else { "AC" } + CalculatorKey { + name: "key-clear", + onclick: move |_| state.write().clear_display(), + if state.read().display_value == "0" { + "C" + } else { + "AC" + } + } + CalculatorKey { + name: "key-sign", + onclick: move |_| state.write().toggle_sign(), + "±" + } + CalculatorKey { + name: "key-percent", + onclick: move |_| state.write().toggle_percent(), + "%" } - CalculatorKey { name: "key-sign", onclick: move |_| state.write().toggle_sign(), "±" } - CalculatorKey { name: "key-percent", onclick: move |_| state.write().toggle_percent(), "%" } } div { class: "digit-keys", - CalculatorKey { name: "key-0", onclick: move |_| state.write().input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| state.write().input_dot(), "●" } + CalculatorKey { + name: "key-0", + onclick: move |_| state.write().input_digit(0), + "0" + } + CalculatorKey { + name: "key-dot", + onclick: move |_| state.write().input_dot(), + "●" + } for k in 1..10 { CalculatorKey { key: "{k}", @@ -74,8 +99,16 @@ fn app() -> Element { onclick: move |_| state.write().set_operator(Operator::Sub), "−" } - CalculatorKey { name: "key-add", onclick: move |_| state.write().set_operator(Operator::Add), "+" } - CalculatorKey { name: "key-equals", onclick: move |_| state.write().perform_operation(), "=" } + CalculatorKey { + name: "key-add", + onclick: move |_| state.write().set_operator(Operator::Add), + "+" + } + CalculatorKey { + name: "key-equals", + onclick: move |_| state.write().perform_operation(), + "=" + } } } } diff --git a/examples/clock.rs b/examples/clock.rs index 678b3c3ab..7150dc7c0 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -5,7 +5,7 @@ use async_std::task::sleep; use dioxus::prelude::*; use web_time::Instant; -const STYLE: &str = asset!("./examples/assets/clock.css"); +const STYLE: Asset = asset!("/examples/assets/clock.css"); fn main() { dioxus::launch(app); diff --git a/examples/control_focus.rs b/examples/control_focus.rs index 9bfdbc3fc..0834f1f5f 100644 --- a/examples/control_focus.rs +++ b/examples/control_focus.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use async_std::task::sleep; use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/roulette.css"); +const STYLE: Asset = asset!("/examples/assets/roulette.css"); fn main() { dioxus::launch(app); diff --git a/examples/counters.rs b/examples/counters.rs index 3e8ff3685..bc9ce327d 100644 --- a/examples/counters.rs +++ b/examples/counters.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/counter.css"); +const STYLE: Asset = asset!("/examples/assets/counter.css"); fn main() { dioxus::launch(app); diff --git a/examples/crm.rs b/examples/crm.rs index 732dd8ee7..294bb33d8 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -22,11 +22,18 @@ fn main() { rsx! { document::Link { rel: "stylesheet", - href: asset!("https://unpkg.com/purecss@2.0.6/build/pure-min.css"), + href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css", integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", - crossorigin: "anonymous" + crossorigin: "anonymous", + } + document::Link { + rel: "stylesheet", + href: asset!("/examples/assets/crm.css"), + } + document::Link { + rel: "stylesheet", + href: asset!("./examples/assets/crm.css"), } - document::Link { rel: "stylesheet", href: asset!("./examples/assets/crm.css") } h1 { "Dioxus CRM Example" } Router:: {} } @@ -117,7 +124,7 @@ fn New() -> Element { placeholder: "Last Name…", required: true, value: "{last_name}", - oninput: move |e| last_name.set(e.value()) + oninput: move |e| last_name.set(e.value()), } } @@ -127,13 +134,21 @@ fn New() -> Element { id: "description", placeholder: "Description…", value: "{description}", - oninput: move |e| description.set(e.value()) + oninput: move |e| description.set(e.value()), } } div { class: "pure-controls", - button { r#type: "submit", class: "pure-button pure-button-primary", "Save" } - Link { to: Route::List, class: "pure-button pure-button-primary red", "Cancel" } + button { + r#type: "submit", + class: "pure-button pure-button-primary", + "Save" + } + Link { + to: Route::List, + class: "pure-button pure-button-primary red", + "Cancel" + } } } } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 58d7677ee..73c9e2fe6 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -1,17 +1,12 @@ //! A simple example on how to use assets loading from the filesystem. //! -//! If the feature "collect-assets" is enabled, the assets will be collected via the dioxus CLI and embedded into the -//! final bundle. This lets you do various useful things like minify, compress, and optimize your assets. -//! -//! We can still use assets without the CLI middleware, but generally larger apps will benefit from it. +//! Dioxus provides the asset!() macro which is a convenient way to load assets from the filesystem. +//! This ensures the asset makes it into the bundle through dependencies and is accessible in environments +//! like web and android where assets are lazily loaded using platform-specific APIs. use dioxus::prelude::*; -#[cfg(not(feature = "collect-assets"))] -static ASSET_PATH: &str = "examples/assets/logo.png"; - -#[cfg(feature = "collect-assets")] -static ASSET_PATH: &str = asset!("examples/assets/logo.png".format(ImageType::Avif)); +static ASSET_PATH: Asset = asset!("/examples/assets/logo.png"); fn main() { dioxus::launch(app); @@ -21,7 +16,7 @@ fn app() -> Element { rsx! { div { h1 { "This should show an image:" } - img { src: ASSET_PATH.to_string() } + img { src: ASSET_PATH } } } } diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs index 6d40f3ada..51efce6bc 100644 --- a/examples/dynamic_asset.rs +++ b/examples/dynamic_asset.rs @@ -7,7 +7,7 @@ use dioxus::desktop::{use_asset_handler, wry::http::Response}; use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/custom_assets.css"); +const STYLE: Asset = asset!("/examples/assets/custom_assets.css"); fn main() { dioxus::LaunchBuilder::desktop().launch(app); diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 1903cf071..8e19d49f1 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use dioxus::prelude::*; use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; -const STYLE: &str = asset!("./examples/assets/file_upload.css"); +const STYLE: Asset = asset!("/examples/assets/file_upload.css"); fn main() { dioxus::launch(app); diff --git a/examples/flat_router.rs b/examples/flat_router.rs index 25dac265b..0da8bc87e 100644 --- a/examples/flat_router.rs +++ b/examples/flat_router.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/flat_router.css"); +const STYLE: Asset = asset!("/examples/assets/flat_router.css"); fn main() { dioxus::launch(|| { diff --git a/examples/fullstack-auth/Cargo.toml b/examples/fullstack-auth/Cargo.toml index a42450dc5..cee550e5d 100644 --- a/examples/fullstack-auth/Cargo.toml +++ b/examples/fullstack-auth/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" publish = false + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/examples/fullstack-hello-world/Cargo.toml b/examples/fullstack-hello-world/Cargo.toml index 16bb88cff..da75ac94b 100644 --- a/examples/fullstack-hello-world/Cargo.toml +++ b/examples/fullstack-hello-world/Cargo.toml @@ -17,5 +17,5 @@ reqwest = { workspace = true } [features] default = [] -server = ["dioxus/axum"] +server = ["dioxus/server"] web = ["dioxus/web"] diff --git a/examples/fullstack-hello-world/assets/hello.css b/examples/fullstack-hello-world/assets/hello.css new file mode 100644 index 000000000..905bc8dc8 --- /dev/null +++ b/examples/fullstack-hello-world/assets/hello.css @@ -0,0 +1,3 @@ +body { + background-color: rgb(108, 104, 104); +} diff --git a/examples/fullstack-hello-world/src/main.rs b/examples/fullstack-hello-world/src/main.rs index b99664eb0..2a1217c40 100644 --- a/examples/fullstack-hello-world/src/main.rs +++ b/examples/fullstack-hello-world/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dx serve --platform fullstack +//! dx serve --platform web //! ``` #![allow(non_snake_case, unused)] @@ -14,6 +14,7 @@ fn app() -> Element { let server_future = use_server_future(get_server_data)?; rsx! { + document::Link { href: asset!("/assets/hello.css"), rel: "stylesheet" } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } diff --git a/examples/fullstack-router/src/main.rs b/examples/fullstack-router/src/main.rs index 9a889e8f6..af691f642 100644 --- a/examples/fullstack-router/src/main.rs +++ b/examples/fullstack-router/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dx serve --platform fullstack +//! dx serve --platform web //! ``` use dioxus::prelude::*; diff --git a/examples/global.rs b/examples/global.rs index c2c4e0ce6..3b7785a6e 100644 --- a/examples/global.rs +++ b/examples/global.rs @@ -7,7 +7,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/counter.css"); +const STYLE: Asset = asset!("/examples/assets/counter.css"); static COUNT: GlobalSignal = Signal::global(|| 0); static DOUBLED_COUNT: GlobalMemo = Memo::global(|| COUNT() * 2); diff --git a/examples/link.rs b/examples/link.rs index 750dd56af..0ce3378df 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -8,7 +8,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/links.css"); +const STYLE: Asset = asset!("/examples/assets/links.css"); fn main() { dioxus::launch(app); diff --git a/examples/meta.rs b/examples/meta.rs index 1030b8354..a01695ffb 100644 --- a/examples/meta.rs +++ b/examples/meta.rs @@ -3,7 +3,6 @@ use dioxus::prelude::*; fn main() { - tracing_subscriber::fmt::init(); dioxus::launch(app); } @@ -12,25 +11,10 @@ fn app() -> Element { // You can use the Meta component to render a meta tag into the head of the page // Meta tags are useful to provide information about the page to search engines and social media sites // This example sets up meta tags for the open graph protocol for social media previews - document::Meta { - property: "og:title", - content: "My Site", - } - document::Meta { - property: "og:type", - content: "website", - } - document::Meta { - property: "og:url", - content: "https://www.example.com", - } - document::Meta { - property: "og:image", - content: "https://example.com/image.jpg", - } - document::Meta { - name: "description", - content: "My Site is a site", - } + document::Meta { property: "og:title", content: "My Site" } + document::Meta { property: "og:type", content: "website" } + document::Meta { property: "og:url", content: "https://www.example.com" } + document::Meta { property: "og:image", content: "https://example.com/image.jpg" } + document::Meta { name: "description", content: "My Site is a site" } } } diff --git a/examples/overlay.rs b/examples/overlay.rs index 3cc0f6a30..879a57a1b 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -24,7 +24,7 @@ fn app() -> Element { rsx! { document::Link { rel: "stylesheet", - href: asset!("./examples/assets/overlay.css"), + href: asset!("/examples/assets/overlay.css"), } if show_overlay() { div { diff --git a/examples/read_size.rs b/examples/read_size.rs index 53f3cd1a9..f4a6fe510 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -27,8 +27,11 @@ fn app() -> Element { } }; - rsx!( - document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") } + rsx! { + document::Link { + rel: "stylesheet", + href: asset!("./examples/assets/read_size.css"), + } div { width: "50%", height: "50%", @@ -38,5 +41,5 @@ fn app() -> Element { } button { onclick: read_dims, "Read dimensions" } - ) + } } diff --git a/examples/reducer.rs b/examples/reducer.rs index 1398ef49d..c1138e748 100644 --- a/examples/reducer.rs +++ b/examples/reducer.rs @@ -7,7 +7,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/radio.css"); +const STYLE: Asset = asset!("/examples/assets/radio.css"); fn main() { dioxus::launch(app); diff --git a/examples/resize.rs b/examples/resize.rs index 783dec407..895b6752d 100644 --- a/examples/resize.rs +++ b/examples/resize.rs @@ -15,7 +15,10 @@ fn app() -> Element { let mut dimensions = use_signal(Size2D::zero); rsx!( - document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") } + document::Link { + rel: "stylesheet", + href: asset!("./examples/assets/read_size.css"), + } div { width: "50%", height: "50%", diff --git a/examples/router.rs b/examples/router.rs index 4a2cc6c5a..1952e764e 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -8,7 +8,7 @@ use dioxus::prelude::*; -const STYLE: &str = asset!("./examples/assets/router.css"); +const STYLE: Asset = asset!("/examples/assets/router.css"); fn main() { dioxus::launch(|| { diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 7a952531b..d871acdd1 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -2,22 +2,19 @@ use dioxus::prelude::*; -const _STYLE: &str = asset!("public/tailwind.css"); - fn main() { dioxus::launch(app); } pub fn app() -> Element { let grey_background = true; - rsx!( + rsx! ( + document::Link { rel: "stylesheet", href: asset!("/public/tailwind.css") } div { header { class: "text-gray-400 body-font", // you can use optional attributes to optionally apply a tailwind class - class: if grey_background { - "bg-gray-900" - }, + class: if grey_background { "bg-gray-900" }, div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", StacksIcon {} @@ -63,7 +60,7 @@ pub fn app() -> Element { class: "object-cover object-center rounded", src: "https://i.imgur.com/oK6BLtw.png", referrerpolicy: "no-referrer", - alt: "hero" + alt: "hero", } } } diff --git a/examples/title.rs b/examples/title.rs index b01b38f4e..f836a7f3d 100644 --- a/examples/title.rs +++ b/examples/title.rs @@ -3,7 +3,6 @@ use dioxus::prelude::*; fn main() { - tracing_subscriber::fmt::init(); dioxus::launch(app); } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8af3ffd9b..f57a8d6c7 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -3,7 +3,7 @@ use dioxus::prelude::*; use std::collections::HashMap; -const STYLE: &str = asset!("./examples/assets/todomvc.css"); +const STYLE: Asset = asset!("/examples/assets/todomvc.css"); fn main() { dioxus::launch(app); diff --git a/packages/autofmt/src/collect_macros.rs b/packages/autofmt/src/collect_macros.rs index 5c518d429..44ec34b65 100644 --- a/packages/autofmt/src/collect_macros.rs +++ b/packages/autofmt/src/collect_macros.rs @@ -28,7 +28,7 @@ impl<'a, 'b> MacroCollector<'a, 'b> { } } -impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> { +impl<'b> Visit<'b> for MacroCollector<'_, 'b> { fn visit_macro(&mut self, i: &'b Macro) { // Visit the regular stuff - this will also ensure paths/attributes are visited syn::visit::visit_macro(self, i); diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index 70ee0711d..67619f93b 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -12,11 +12,6 @@ pub const ASSET_ROOT_ENV: &str = "DIOXUS_ASSET_ROOT"; pub const APP_TITLE_ENV: &str = "DIOXUS_APP_TITLE"; pub const OUT_DIR: &str = "DIOXUS_OUT_DIR"; -/// todo: this is not implemented but we're going to reserve this -/// -/// technically this is only passed on "launch" so if you close the app, this will be lost -pub const IOS_DEVSERVER_ADDR_ENV: &str = "SIMCTL_CHILD_DIOXUS_DEVSERVER_ADDR"; - /// Get the address of the devserver for use over a raw socket /// /// This is not a websocket! There's no protocol! diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index d57fc57ac..663137ca3 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -10,19 +10,25 @@ keywords = ["react", "gui", "cli", "dioxus", "wasm"] rust-version = "1.79.0" [dependencies] -# cli core -clap = { version = "4.2", features = ["derive", "cargo"] } +dioxus-autofmt = { workspace = true } +dioxus-check = { workspace = true } +dioxus-rsx-rosetta = { workspace = true } +dioxus-rsx = { workspace = true } +dioxus-rsx-hotreload = { workspace = true } +dioxus-html = { workspace = true, features = ["hot-reload-context"] } +dioxus-core = { workspace = true, features = ["serialize"] } +dioxus-core-types = { workspace = true } +dioxus-devtools-types = { workspace = true } +dioxus-cli-config = { workspace = true } +dioxus-fullstack = { workspace = true } + +clap = { workspace = true, features = ["derive", "cargo"] } thiserror = { workspace = true } wasm-bindgen-cli-support = "0.2" wasm-bindgen-shared = "0.2" -colored = "2.0.0" -dioxus-cli-config = { workspace = true } - -# features -log = "0.4.14" -fern = { version = "0.6.0", features = ["colored"] } -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.79" +uuid = { version = "1.3.0", features = ["v4"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } toml = { workspace = true } fs_extra = "1.2.0" cargo_toml = { workspace = true } @@ -30,7 +36,7 @@ futures-util = { workspace = true, features = ["async-await-macro"] } notify = { workspace = true, features = ["serde"] } html_parser = { workspace = true } cargo_metadata = "0.18.1" -tokio = { workspace = true, features = ["fs", "sync", "rt", "macros", "process", "rt-multi-thread"] } +tokio = { workspace = true, features = ["full"] } tokio-stream = "0.1.15" chrono = "0.4.19" anyhow = "1" @@ -38,12 +44,12 @@ hyper = { workspace = true } hyper-util = "0.1.3" hyper-rustls = { workspace = true } rustls = { workspace = true } -subprocess = "0.2.9" rayon = "1.8.0" console = "0.15.8" ctrlc = "3.2.3" futures-channel = { workspace = true } krates = { version = "0.17.0" } +cargo-config2 = { workspace = true, optional = true } regex = "1.10.6" axum = { workspace = true, features = ["ws"] } @@ -53,7 +59,7 @@ tower-http = { workspace = true, features = ["full"] } proc-macro2 = { workspace = true, features = ["span-locations"] } syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] } -headers = "0.3.7" +headers = "0.4.0" walkdir = "2" # tools download @@ -75,42 +81,39 @@ open = "5.0.1" cargo-generate = "=0.21.3" toml_edit = "0.22.20" -# bundling -tauri-bundler = { workspace = true } -tauri-utils = { workspace = true } - # formatting # syn = { workspace = true } prettyplease = { workspace = true } # Assets -manganis-cli-support = { workspace = true, features = ["html"] } brotli = "6.0.0" - -dioxus-autofmt = { workspace = true } -dioxus-check = { workspace = true } -dioxus-rsx-rosetta = { workspace = true } -dioxus-rsx = { workspace = true } -dioxus-rsx-hotreload = { workspace = true } -dioxus-html = { workspace = true, features = ["hot-reload-context"] } -dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-core-types = { workspace = true } -dioxus-devtools = { workspace = true } ignore = "0.4.22" -env_logger = "0.11.3" +env_logger = { workspace = true } tracing-subscriber = { version = "0.3.18", features = ["std", "env-filter"] } console-subscriber = { version = "0.3.0", optional = true } tracing = { workspace = true } wasm-opt = { version = "0.116.1", optional = true } -ratatui = { version = "0.27.0", features = ["crossterm", "unstable"] } -crossterm = { version = "0.27.0", features = ["event-stream"] } -ansi-to-tui = "=5.0.0-rc.1" +crossterm = { version = "0.28.0", features = ["event-stream"] } +ansi-to-tui = "6.0" ansi-to-html = "0.2.1" +ratatui = { version = "0.28.0", features = ["crossterm", "unstable"] } -# on macos, we need to specify the vendored feature on ssl when cross compiling -# [target.'cfg(target_os = "macos")'.dependencies] -# openssl = { version = "0.10", features = ["vendored"] } +# link intercept +tempfile = "3.3" +manganis-core = { workspace = true } + +# Extracting data from an executable +object = {version="0.36.0", features=["wasm"]} +tokio-util = { version = "0.7.11", features = ["full"] } +itertools = "0.13.0" +throbber-widgets-tui = "0.7.0" +unicode-segmentation = "1.12.0" +handlebars = "6.1.0" +strum = { version = "0.26.3", features = ["derive"] } + +tauri-utils = { workspace = true } +tauri-bundler = { workspace = true } [build-dependencies] built = { version = "=0.7.4", features = ["git2"] } @@ -119,24 +122,27 @@ built = { version = "=0.7.4", features = ["git2"] } default = [] plugin = [] tokio-console = ["dep:console-subscriber"] -optimizations = ["dep:wasm-opt"] +bundle = [] + +# when releasing dioxus, we want to enable wasm-opt +# and then also maybe developing it too. +# making this optional cuts workspace deps down from 1000 to 500, so it's very nice for workspace adev +optimizations = ["wasm-opt", "asset-opt"] +asset-opt = [] +wasm-opt = ["dep:wasm-opt"] [[bin]] path = "src/main.rs" name = "dx" -[dev-dependencies] -tempfile = "3.3" - [package.metadata.binstall] -# temporarily, we're going to use the 0.5.0 download page for all binaries pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }-v{ version }{ archive-suffix }" - -# the old one... -# pkg-url = "{ repo }/releases/download/v0.5.0/dx-{ target }-v{ version }{ archive-suffix }" - -# pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }" pkg-fmt = "tgz" [package.metadata.binstall.overrides.x86_64-pc-windows-msvc] pkg-fmt = "zip" + +[package.metadata.docs.rs] +all-features = false +rustc-args = [ "--cfg", "docsrs" ] +rustdoc-args = [ "--cfg", "docsrs" ] diff --git a/packages/cli/assets/android/.gitignore b/packages/cli/assets/android/.gitignore new file mode 100644 index 000000000..f423e6060 --- /dev/null +++ b/packages/cli/assets/android/.gitignore @@ -0,0 +1 @@ +app-dir/ diff --git a/packages/cli/assets/ios/ios.plist b/packages/cli/assets/ios/ios.plist new file mode 100644 index 000000000..a5da859d6 --- /dev/null +++ b/packages/cli/assets/ios/ios.plist @@ -0,0 +1,25 @@ + + + + + + CFBundleIdentifier + com.dioxuslabs + CFBundleDisplayName + DioxusApp + CFBundleName + DioxusApp + CFBundleExecutable + DioxusApp + CFBundleVersion + 0.1.0 + CFBundleShortVersionString + 0.1.0 + CFBundleDevelopmentRegion + en_US + UILaunchStoryboardName + + LSRequiresIPhoneOS + + + diff --git a/packages/cli/assets/macos/mac.plist b/packages/cli/assets/macos/mac.plist new file mode 100644 index 000000000..20c877f7f --- /dev/null +++ b/packages/cli/assets/macos/mac.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDisplayName + DioxusApp + + CFBundleExecutable + DioxusApp + + CFBundleIconFile + icon.icns + + CFBundleIdentifier + org.dioxuslabs.dioxus-desktop + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundleName + App + + CFBundlePackageType + APPL + + CFBundleShortVersionString + 7.4.0 + + CFBundleVersion + 1 + + LSApplicationCategoryType + public.app-category.social-networking + + LSMinimumSystemVersion + 10.15 + + diff --git a/packages/cli/assets/index.html b/packages/cli/assets/web/index.html similarity index 100% rename from packages/cli/assets/index.html rename to packages/cli/assets/web/index.html diff --git a/packages/cli/assets/loading.html b/packages/cli/assets/web/loading.html similarity index 100% rename from packages/cli/assets/loading.html rename to packages/cli/assets/web/loading.html diff --git a/packages/cli/assets/web/loading.js b/packages/cli/assets/web/loading.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cli/assets/toast.html b/packages/cli/assets/web/toast.html similarity index 100% rename from packages/cli/assets/toast.html rename to packages/cli/assets/web/toast.html diff --git a/packages/cli/src/assets.rs b/packages/cli/src/assets.rs index 08b8e55c3..c05e8c101 100644 --- a/packages/cli/src/assets.rs +++ b/packages/cli/src/assets.rs @@ -1,209 +1,113 @@ -use crate::builder::{BuildRequest, Stage, UpdateBuildProgress, UpdateStage}; -use crate::Result; -use crate::TraceSrc; use anyhow::Context; -use brotli::enc::BrotliEncoderParams; -use futures_channel::mpsc::UnboundedSender; -use manganis_cli_support::{process_file, AssetManifest, AssetManifestExt, AssetType}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use std::fs; +use manganis_core::{LinkSection, ResourceAsset}; +use object::{read::archive::ArchiveFile, File as ObjectFile, Object, ObjectSection}; +use serde::{Deserialize, Serialize}; use std::path::Path; -use std::sync::atomic::AtomicUsize; -use std::sync::Arc; -use std::{ffi::OsString, path::PathBuf}; -use std::{fs::File, io::Write}; -use walkdir::WalkDir; +use std::{collections::HashMap, path::PathBuf}; -/// The temp file name for passing manganis json from linker to current exec. -pub const MG_JSON_OUT: &str = "mg-out"; - -pub fn asset_manifest(build: &BuildRequest) -> Option { - let file_path = build.target_out_dir().join(MG_JSON_OUT); - let read = fs::read_to_string(&file_path).ok()?; - _ = fs::remove_file(file_path); - let json: Vec = serde_json::from_str(&read).unwrap(); - - Some(AssetManifest::load(json)) +/// A manifest of all assets collected from dependencies +/// +/// This will be filled in primarily by incremental compilation artifacts. +#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)] +pub(crate) struct AssetManifest { + /// Map of bundled asset name to the asset itself + pub(crate) assets: HashMap, } -/// Create a head file that contains all of the imports for assets that the user project uses -pub fn create_assets_head(build: &BuildRequest, manifest: &AssetManifest) -> Result<()> { - let out_dir = build.target_out_dir(); - std::fs::create_dir_all(&out_dir)?; - let mut file = File::create(out_dir.join("__assets_head.html"))?; - file.write_all(manifest.head().as_bytes())?; - Ok(()) -} +impl AssetManifest { + pub(crate) fn load_from_file(path: &Path) -> anyhow::Result { + let src = std::fs::read_to_string(path) + .context("Failed to read asset manifest from filesystem")?; + serde_json::from_str(&src) + .with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}")) + } -/// Process any assets collected from the binary -pub(crate) fn process_assets( - build: &BuildRequest, - manifest: &AssetManifest, - progress: &mut UnboundedSender, -) -> anyhow::Result<()> { - let static_asset_output_dir = build.target_out_dir(); - - std::fs::create_dir_all(&static_asset_output_dir) - .context("Failed to create static asset output directory")?; - - let assets_finished = Arc::new(AtomicUsize::new(0)); - let assets = manifest.assets(); - let asset_count = assets.len(); - assets.par_iter().try_for_each_init( - || progress.clone(), - move |progress, asset| { - if let AssetType::File(file_asset) = asset { - match process_file(file_asset, &static_asset_output_dir) { - Ok(_) => { - // Update the progress - tracing::info!(dx_src = ?TraceSrc::Build, "Optimized static asset {file_asset}"); - let assets_finished = - assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::OptimizingAssets, - update: UpdateStage::SetProgress( - assets_finished as f64 / asset_count as f64, - ), - }); - } - Err(err) => { - tracing::error!(dx_src = ?TraceSrc::Build, "Failed to copy static asset: {}", err); - return Err(err); - } - } - } - Ok::<(), anyhow::Error>(()) - }, - )?; - - Ok(()) -} - -/// A guard that sets up the environment for the web renderer to compile in. This guard sets the location that assets will be served from -pub(crate) struct AssetConfigDropGuard; - -impl AssetConfigDropGuard { - pub fn new(base_path: Option<&str>) -> Self { - // Set up the collect asset config - let base = match base_path { - Some(base) => format!("/{}/", base.trim_matches('/')), - None => "/".to_string(), + /// Fill this manifest with a file object/rlib files, typically extracted from the linker intercepted + pub(crate) fn add_from_object_path(&mut self, path: PathBuf) -> anyhow::Result<()> { + let Some(ext) = path.extension() else { + return Ok(()); }; - manganis_cli_support::Config::default() - .with_assets_serve_location(base) - .save(); - Self {} - } -} -impl Drop for AssetConfigDropGuard { - fn drop(&mut self) { - // Reset the config - manganis_cli_support::Config::default().save(); - } -} + let Some(ext) = ext.to_str() else { + return Ok(()); + }; -pub(crate) fn copy_dir_to( - src_dir: PathBuf, - dest_dir: PathBuf, - pre_compress: bool, -) -> std::io::Result<()> { - let entries = std::fs::read_dir(&src_dir)?; - let mut children: Vec>> = Vec::new(); + let data = std::fs::read(path.clone())?; - for entry in entries.flatten() { - let entry_path = entry.path(); - let path_relative_to_src = entry_path.strip_prefix(&src_dir).unwrap(); - let output_file_location = dest_dir.join(path_relative_to_src); - children.push(std::thread::spawn(move || { - if entry.file_type()?.is_dir() { - // If the file is a directory, recursively copy it into the output directory - if let Err(err) = - copy_dir_to(entry_path.clone(), output_file_location, pre_compress) - { - tracing::error!( - dx_src = ?TraceSrc::Build, - "Failed to pre-compress directory {}: {}", - entry_path.display(), - err - ); - } - } else { - // Make sure the directory exists - std::fs::create_dir_all(output_file_location.parent().unwrap())?; - // Copy the file to the output directory - std::fs::copy(&entry_path, &output_file_location)?; - - // Then pre-compress the file if needed - if pre_compress { - if let Err(err) = pre_compress_file(&output_file_location) { - tracing::error!( - dx_src = ?TraceSrc::Build, - "Failed to pre-compress static assets {}: {}", - output_file_location.display(), - err - ); - } - // If pre-compression isn't enabled, we should remove the old compressed file if it exists - } else if let Some(compressed_path) = compressed_path(&output_file_location) { - _ = std::fs::remove_file(compressed_path); + match ext { + // Parse an unarchived object file + "o" => { + if let Ok(object) = object::File::parse(&*data) { + self.add_from_object_file(&object)?; } } - Ok(()) - })); - } - for child in children { - child.join().unwrap()?; - } - Ok(()) -} - -/// Get the path to the compressed version of a file -fn compressed_path(path: &Path) -> Option { - let new_extension = match path.extension() { - Some(ext) => { - if ext.to_string_lossy().to_lowercase().ends_with("br") { - return None; + // Parse an rlib as a collection of objects + "rlib" => { + if let Ok(archive) = object::read::archive::ArchiveFile::parse(&*data) { + self.add_from_archive_file(&archive, &data)?; + } } - let mut ext = ext.to_os_string(); - ext.push(".br"); - ext + _ => {} } - None => OsString::from("br"), - }; - Some(path.with_extension(new_extension)) -} -/// pre-compress a file with brotli -pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> { - let Some(compressed_path) = compressed_path(path) else { - return Ok(()); - }; - let file = std::fs::File::open(path)?; - let mut stream = std::io::BufReader::new(file); - let mut buffer = std::fs::File::create(compressed_path)?; - let params = BrotliEncoderParams::default(); - brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?; - Ok(()) -} + Ok(()) + } -/// pre-compress all files in a folder -pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::Result<()> { - let walk_dir = WalkDir::new(path); - for entry in walk_dir.into_iter().filter_map(|e| e.ok()) { - let entry_path = entry.path(); - if entry_path.is_file() { - if pre_compress { - if let Err(err) = pre_compress_file(entry_path) { - tracing::error!(dx_src = ?TraceSrc::Build, "Failed to pre-compress file {entry_path:?}: {err}"); - } - } - // If pre-compression isn't enabled, we should remove the old compressed file if it exists - else if let Some(compressed_path) = compressed_path(entry_path) { - _ = std::fs::remove_file(compressed_path); + /// Fill this manifest from an rlib / ar file that contains many object files and their entryies + fn add_from_archive_file(&mut self, archive: &ArchiveFile, data: &[u8]) -> object::Result<()> { + // Look through each archive member for object files. + // Read the archive member's binary data (we know it's an object file) + // And parse it with the normal `object::File::parse` to find the manganis string. + for member in archive.members() { + let member = member?; + let name = String::from_utf8_lossy(member.name()).to_string(); + + // Check if the archive member is an object file and parse it. + if name.ends_with(".o") { + let data = member.data(data)?; + let object = object::File::parse(data)?; + _ = self.add_from_object_file(&object); } } + + Ok(()) + } + + /// Fill this manifest with whatever tables might come from the object file + fn add_from_object_file(&mut self, obj: &ObjectFile) -> anyhow::Result<()> { + for section in obj.sections() { + let Ok(section_name) = section.name() else { + continue; + }; + + // Check if the link section matches the asset section for one of the platforms we support. This may not be the current platform if the user is cross compiling + let matches = LinkSection::ALL + .iter() + .any(|x| x.link_section == section_name); + + if !matches { + continue; + } + + let bytes = section + .uncompressed_data() + .context("Could not read uncompressed data from object file")?; + + let as_str = std::str::from_utf8(&bytes) + .context("object file contained non utf8 encoding")? + .chars() + .filter(|c| !c.is_control()) + .collect::(); + + let assets = serde_json::Deserializer::from_str(&as_str).into_iter::(); + for as_resource in assets.flatten() { + // Some platforms (e.g. macOS) start the manganis section with a null byte, we need to filter that out before we deserialize the JSON + self.assets + .insert(as_resource.absolute.clone(), as_resource); + } + } + + Ok(()) } - Ok(()) } diff --git a/packages/cli/src/builder/build.rs b/packages/cli/src/builder/build.rs new file mode 100644 index 000000000..84afdf65c --- /dev/null +++ b/packages/cli/src/builder/build.rs @@ -0,0 +1,404 @@ +use super::{progress::ProgressTx, BuildArtifacts}; +use crate::dioxus_crate::DioxusCrate; +use crate::Result; +use crate::{assets::AssetManifest, TraceSrc}; +use crate::{build::BuildArgs, link::LinkAction}; +use crate::{AppBundle, Platform}; +use anyhow::Context; +use serde::Deserialize; +use std::{path::PathBuf, process::Stdio}; +use tokio::{io::AsyncBufReadExt, process::Command}; + +#[derive(Clone, Debug)] +pub(crate) struct BuildRequest { + /// The configuration for the crate we are building + pub(crate) krate: DioxusCrate, + + /// The arguments for the build + pub(crate) build: BuildArgs, + + /// Status channel to send our progress updates to + pub(crate) progress: ProgressTx, + + /// The target directory for the build + pub(crate) custom_target_dir: Option, +} + +impl BuildRequest { + pub fn new(krate: DioxusCrate, build: BuildArgs, progress: ProgressTx) -> Self { + Self { + build, + krate, + progress, + custom_target_dir: None, + } + } + + /// Run the build command with a pretty loader, returning the executable output location + /// + /// This will also run the fullstack build. Note that fullstack is handled separately within this + /// code flow rather than outside of it. + pub(crate) async fn build_all(self) -> Result { + tracing::debug!("Running build command..."); + + let (app, server) = + futures_util::future::try_join(self.build_app(), self.build_server()).await?; + + AppBundle::new(self, app, server).await + } + + pub(crate) async fn build_app(&self) -> Result { + tracing::debug!("Building app..."); + let exe = self.build_cargo().await?; + let assets = self.collect_assets().await?; + Ok(BuildArtifacts { exe, assets }) + } + + pub(crate) async fn build_server(&self) -> Result> { + tracing::debug!("Building server..."); + + if !self.build.fullstack { + return Ok(None); + } + + let mut cloned = self.clone(); + cloned.build.platform = Some(Platform::Server); + Ok(Some(cloned.build_app().await?)) + } + + /// Run `cargo`, returning the location of the final executable + /// + /// todo: add some stats here, like timing reports, crate-graph optimizations, etc + pub(crate) async fn build_cargo(&self) -> Result { + tracing::debug!("Executing cargo..."); + + // Extract the unit count of the crate graph so build_cargo has more accurate data + let crate_count = self.get_unit_count_estimate().await; + + // Update the status to show that we're starting the build and how many crates we expect to build + self.status_starting_build(crate_count); + + let mut cmd = Command::new("cargo"); + + cmd.arg("rustc") + .current_dir(self.krate.crate_dir()) + .arg("--message-format") + .arg("json-diagnostic-rendered-ansi") + .args(self.build_arguments()) + .env("RUSTFLAGS", self.rust_flags()); + + if let Some(target_dir) = self.custom_target_dir.as_ref() { + cmd.env("CARGO_TARGET_DIR", target_dir); + } + + // Android needs a special linker since the linker is actually tied to the android toolchain. + // For the sake of simplicity, we're going to pass the linker here using ourselves as the linker, + // but in reality we could simply use the android toolchain's linker as the path. + // + // We don't want to overwrite the user's .cargo/config.toml since that gets committed to git + // and we want everyone's install to be the same. + if self.build.platform() == Platform::Android { + cmd.env( + LinkAction::ENV_VAR_NAME, + LinkAction::LinkAndroid { + linker: "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang".into(), + extra_flags: vec![], + } + .to_json(), + ); + } + + tracing::trace!(dx_src = ?TraceSrc::Build, "Rust cargo args: {:?}", cmd); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to spawn cargo build")?; + + let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap()); + let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap()); + let mut output_location = None; + let mut stdout = stdout.lines(); + let mut stderr = stderr.lines(); + let mut units_compiled = 0; + + loop { + use cargo_metadata::Message; + + let line = tokio::select! { + Ok(Some(line)) = stdout.next_line() => line, + Ok(Some(line)) = stderr.next_line() => line, + else => break, + }; + + let mut deserializer = serde_json::Deserializer::from_str(line.trim()); + deserializer.disable_recursion_limit(); + + let message = + Message::deserialize(&mut deserializer).unwrap_or(Message::TextLine(line)); + + match message { + Message::BuildScriptExecuted(_) => units_compiled += 1, + Message::TextLine(line) => self.status_build_message(line), + Message::CompilerMessage(msg) => self.status_build_diagnostic(msg), + Message::CompilerArtifact(artifact) => { + units_compiled += 1; + match artifact.executable { + Some(executable) => output_location = Some(executable.into()), + None => self.status_build_progress( + units_compiled, + crate_count, + artifact.target.name, + self.build.platform(), + ), + } + } + Message::BuildFinished(finished) => { + if !finished.success { + return Err(anyhow::anyhow!( + "Cargo build failed, signaled by the compiler" + ) + .into()); + } + } + _ => {} + } + } + + if output_location.is_none() { + tracing::error!("Cargo build failed - no output location"); + } + + let out_location = output_location.context("Build did not return an executable")?; + + tracing::debug!( + "Build completed successfully - output location: {:?}", + out_location + ); + + Ok(out_location) + } + + /// Run the linker intercept and then fill in our AssetManifest from the incremental artifacts + /// + /// This will execute `dx` with an env var set to force `dx` to operate as a linker, and then + /// traverse the .o and .rlib files rustc passes that new `dx` instance, collecting the link + /// tables marked by manganis and parsing them as a ResourceAsset. + pub(crate) async fn collect_assets(&self) -> anyhow::Result { + tracing::debug!("Collecting assets ..."); + + // If assets are skipped, we don't need to collect them + if self.build.skip_assets { + return Ok(AssetManifest::default()); + } + + // Create a temp file to put the output of the args + // We need to do this since rustc won't actually print the link args to stdout, so we need to + // give `dx` a file to dump its env::args into + let tmp_file = tempfile::NamedTempFile::new()?; + + // Run `cargo rustc` again, but this time with a custom linker (dx) and an env var to force + // `dx` to act as a linker + // + // This will force `dx` to look through the incremental cache and find the assets from the previous build + Command::new("cargo") + // .env("RUSTFLAGS", self.rust_flags()) + .arg("rustc") + .args(self.build_arguments()) + .arg("--offline") /* don't use the network, should already be resolved */ + .arg("--") + .arg(format!( + "-Clinker={}", + std::env::current_exe() + .unwrap() + .canonicalize() + .unwrap() + .display() + )) + .env( + LinkAction::ENV_VAR_NAME, + LinkAction::BuildAssetManifest { + destination: tmp_file.path().to_path_buf(), + } + .to_json(), + ) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await?; + + // The linker wrote the manifest to the temp file, let's load it! + AssetManifest::load_from_file(tmp_file.path()) + } + + /// Create a list of arguments for cargo builds + pub(crate) fn build_arguments(&self) -> Vec { + let mut cargo_args = Vec::new(); + + // Set the target, profile and features that vary between the app and server builds + if self.build.platform() == Platform::Server { + cargo_args.push("--profile".to_string()); + cargo_args.push(self.build.server_profile.to_string()); + } else { + if let Some(custom_profile) = &self.build.profile { + cargo_args.push("--profile".to_string()); + cargo_args.push(custom_profile.to_string()); + } + + // todo: use the right arch based on the current arch + let custom_target = match self.build.platform() { + Platform::Web => Some("wasm32-unknown-unknown"), + Platform::Ios => match self.build.target_args.device { + Some(true) => Some("aarch64-apple-ios"), + _ => Some("aarch64-apple-ios-sim"), + }, + Platform::Android => Some("aarch64-linux-android"), + Platform::Server => None, + // we're assuming we're building for the native platform for now... if you're cross-compiling + // the targets here might be different + Platform::MacOS => None, + Platform::Windows => None, + Platform::Linux => None, + Platform::Liveview => None, + }; + + if let Some(target) = custom_target.or(self.build.target_args.target.as_deref()) { + cargo_args.push("--target".to_string()); + cargo_args.push(target.to_string()); + } + } + + if self.build.release { + cargo_args.push("--release".to_string()); + } + + if self.build.verbose { + cargo_args.push("--verbose".to_string()); + } else { + cargo_args.push("--quiet".to_string()); + } + + let features = self.target_features(); + + if !features.is_empty() { + cargo_args.push("--features".to_string()); + cargo_args.push(features.join(" ")); + } + + if let Some(ref package) = self.build.target_args.package { + cargo_args.push(String::from("-p")); + cargo_args.push(package.clone()); + } + + cargo_args.append(&mut self.build.cargo_args.clone()); + + match self.krate.executable_type() { + krates::cm::TargetKind::Bin => cargo_args.push("--bin".to_string()), + krates::cm::TargetKind::Lib => cargo_args.push("--lib".to_string()), + krates::cm::TargetKind::Example => cargo_args.push("--example".to_string()), + _ => {} + }; + + cargo_args.push(self.krate.executable_name().to_string()); + + tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args); + + cargo_args + } + + pub(crate) fn rust_flags(&self) -> String { + let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default(); + + if self.build.platform() == Platform::Android { + let cur_exe = std::env::current_exe().unwrap(); + rust_flags.push_str(format!(" -Clinker={}", cur_exe.display()).as_str()); + rust_flags.push_str(" -Clink-arg=-landroid"); + rust_flags.push_str(" -Clink-arg=-llog"); + rust_flags.push_str(" -Clink-arg=-lOpenSLES"); + rust_flags.push_str(" -Clink-arg=-Wl,--export-dynamic"); + } + + rust_flags + } + + /// Create the list of features we need to pass to cargo to build the app by merging together + /// either the client or server features depending on if we're building a server or not. + pub(crate) fn target_features(&self) -> Vec { + let mut features = self.build.target_args.features.clone(); + + if self.build.platform() == Platform::Server { + features.extend(self.build.target_args.server_features.clone()); + } else { + features.extend(self.build.target_args.client_features.clone()); + } + + features + } + + pub(crate) fn all_target_features(&self) -> Vec { + let mut features = self.target_features(); + + if !self.build.target_args.no_default_features { + features.extend( + self.krate + .package() + .features + .get("default") + .cloned() + .unwrap_or_default(), + ); + } + + features.dedup(); + + features + } + + /// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed. + pub(crate) async fn get_unit_count(&self) -> crate::Result { + #[derive(Debug, Deserialize)] + struct UnitGraph { + units: Vec, + } + + let output = tokio::process::Command::new("cargo") + .arg("+nightly") + .arg("build") + .arg("--unit-graph") + .arg("-Z") + .arg("unstable-options") + .args(self.build_arguments()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Failed to get unit count").into()); + } + + let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?; + let graph: UnitGraph = + serde_json::from_str(&output_text).context("Failed to get unit count")?; + + Ok(graph.units.len()) + } + + /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata. + /// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable + pub(crate) async fn get_unit_count_estimate(&self) -> usize { + // Try to get it from nightly + self.get_unit_count().await.unwrap_or_else(|_| { + // Otherwise, use cargo metadata + (self + .krate + .krates + .krates_filtered(krates::DepKind::Dev) + .iter() + .map(|k| k.targets.len()) + .sum::() as f64 + / 3.5) as usize + }) + } +} diff --git a/packages/cli/src/builder/bundle.rs b/packages/cli/src/builder/bundle.rs new file mode 100644 index 000000000..a2e47a1dc --- /dev/null +++ b/packages/cli/src/builder/bundle.rs @@ -0,0 +1,722 @@ +use crate::Result; +use crate::{assets::AssetManifest, TraceSrc}; +use crate::{BuildRequest, Platform}; +use anyhow::Context; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use std::sync::atomic::AtomicUsize; +use std::{ + fs::create_dir_all, + path::{Path, PathBuf}, +}; +use wasm_bindgen_cli_support::Bindgen; + +/// The end result of a build. +/// +/// Contains the final asset manifest, the executables, and the workdir. +/// +/// Every dioxus app can have an optional server executable which will influence the final bundle. +/// This is built in parallel with the app executable during the `build` phase and the progres/status +/// of the build is aggregated. +/// +/// The server will *always* be dropped into the `web` folder since it is considered "web" in nature, +/// and will likely need to be combined with the public dir to be useful. +/// +/// We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform +/// can just use the build dir +/// +/// When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name: +/// ``` +/// dog-app/ +/// build/ +/// web/ +/// server.exe +/// assets/ +/// some-secret-asset.txt (a server-side asset) +/// public/ +/// index.html +/// assets/ +/// logo.png +/// desktop/ +/// App.app +/// App.appimage +/// App.exe +/// server/ +/// server +/// assets/ +/// some-secret-asset.txt (a server-side asset) +/// ios/ +/// App.app +/// App.ipa +/// android/ +/// App.apk +/// bundle/ +/// build.json +/// Desktop.app +/// Mobile_x64.ipa +/// Mobile_arm64.ipa +/// Mobile_rosetta.ipa +/// web.appimage +/// web/ +/// server.exe +/// assets/ +/// some-secret-asset.txt +/// public/ +/// index.html +/// assets/ +/// logo.png +/// style.css +/// ``` +/// +/// When deploying, the build.json file will provide all the metadata that dx-deploy will use to +/// push the app to stores, set up infra, manage versions, etc. +/// +/// The format of each build will follow the name plus some metadata such that when distributing you +/// can easily trim off the metadata. +/// +/// The idea here is that we can run any of the programs in the same way that they're deployed. +/// +/// +/// ## Bundle structure links +/// - apple: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle +/// - appimage: https://docs.appimage.org/packaging-guide/manual.html#ref-manual +/// +/// ## Extra links +/// - xbuild: https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs +#[derive(Debug)] +pub(crate) struct AppBundle { + pub(crate) build: BuildRequest, + pub(crate) app: BuildArtifacts, + pub(crate) server: Option, +} + +#[derive(Debug)] +pub struct BuildArtifacts { + pub(crate) exe: PathBuf, + pub(crate) assets: AssetManifest, +} + +impl AppBundle { + /// ## Web: + /// Create a folder that is somewhat similar to an app-image (exe + asset) + /// The server is dropped into the `web` folder, even if there's no `public` folder. + /// If there's no server (SPA/static-gen), we still use the `web` folder, but it only contains the + /// public folder. + /// ``` + /// web/ + /// server + /// assets/ + /// public/ + /// index.html + /// wasm/ + /// app.wasm + /// glue.js + /// snippets/ + /// ... + /// assets/ + /// logo.png + /// ``` + /// + /// ## Linux: + /// https://docs.appimage.org/reference/appdir.html#ref-appdir + /// current_exe.join("Assets") + /// ``` + /// app.appimage/ + /// AppRun + /// app.desktop + /// package.json + /// assets/ + /// logo.png + /// ``` + /// + /// ## Macos + /// We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources` + /// We put assets in an assets dir such that it generally matches every other platform and we can + /// output `/assets/blah` from manganis. + /// ``` + /// App.app/ + /// Contents/ + /// Info.plist + /// MacOS/ + /// Frameworks/ + /// Resources/ + /// assets/ + /// blah.icns + /// blah.png + /// CodeResources + /// _CodeSignature/ + /// ``` + /// + /// ## iOS + /// Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably + /// since most ios apps don't ship frameworks/plugins and such. + /// + /// todo(jon): include the signing and entitlements in this format diagram. + /// ``` + /// App.app/ + /// main + /// assets/ + /// ``` + /// + /// ## Android: + /// + /// Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since + /// we need to compile kotlin and java. This pushes us into using gradle and following a structure + /// similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and + /// drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires + /// us to manage dependencies (like kotlinc) ourselves (yuck!). + /// + /// https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh + /// + /// Unfortunately, it seems that while we can drop the `android` build plugin, we still will need + /// gradle since kotlin is basically gradle-only. + /// + /// Pre-build: + /// ``` + /// app.apk/ + /// .gradle + /// app/ + /// src/ + /// main/ + /// assets/ + /// jniLibs/ + /// java/ + /// kotlin/ + /// res/ + /// AndroidManifest.xml + /// build.gradle.kts + /// proguard-rules.pro + /// buildSrc/ + /// build.gradle.kts + /// src/ + /// main/ + /// kotlin/ + /// BuildTask.kt + /// build.gradle.kts + /// gradle.properties + /// gradlew + /// gradlew.bat + /// settings.gradle + /// ``` + /// + /// Final build: + /// ``` + /// app.apk/ + /// AndroidManifest.xml + /// classes.dex + /// assets/ + /// logo.png + /// lib/ + /// armeabi-v7a/ + /// libmyapp.so + /// arm64-v8a/ + /// libmyapp.so + /// ``` + /// Notice that we *could* feasibly build this ourselves :) + /// + /// ## Windows: + /// https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows + /// Windows does not provide an AppImage format, so instead we're going build the same folder + /// structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources + /// as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the + /// Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe + /// entrypoint in the associated Program Files folder. + /// + /// This is, in essence, the same as an installer, so we might eventually just support something like msi/msix + /// which functionally do the same thing but with a sleeker UI. + /// + /// This means no installers are required and we can bake an updater into the host exe. + /// + /// ## Handling asset lookups: + /// current_exe.join("assets") + /// ``` + /// app.appimage/ + /// main.exe + /// main.desktop + /// package.json + /// assets/ + /// logo.png + /// ``` + /// + /// Since we support just a few locations, we could just search for the first that exists + /// - usr + /// - ../Resources + /// - assets + /// - Assets + /// - $cwd/assets + /// + /// ``` + /// assets::root() -> + /// mac -> ../Resources/ + /// ios -> ../Resources/ + /// android -> assets/ + /// server -> assets/ + /// liveview -> assets/ + /// web -> /assets/ + /// root().join(bundled) + /// ``` + pub(crate) async fn new( + request: BuildRequest, + app: BuildArtifacts, + server: Option, + ) -> Result { + let bundle = Self { + app, + server, + build: request, + }; + + tracing::debug!("Assembling app bundle"); + + bundle.build.status_start_bundle(); + bundle.prepare_build_dir()?; + bundle.write_main_executable().await?; + bundle.write_server_executable().await?; + bundle.write_assets().await?; + bundle.write_metadata().await?; + bundle.optimize().await?; + + Ok(bundle) + } + + /// We only really currently care about: + /// + /// - app dir (.app, .exe, .apk, etc) + /// - assets dir + /// - exe dir (.exe, .app, .apk, etc) + /// - extra scaffolding + /// + /// It's not guaranteed that they're different from any other folder + fn prepare_build_dir(&self) -> Result<()> { + create_dir_all(self.app_dir())?; + create_dir_all(self.exe_dir())?; + create_dir_all(self.asset_dir())?; + + // we could download the templates from somewhere (github?) but after having banged my head against + // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates + // by writing them here. + if let Platform::Android = self.build.build.platform() {} + + Ok(()) + } + + /// Take the output of rustc and make it into the main exe of the bundle + /// + /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations + /// Other platforms we might do some stripping or other optimizations + /// Move the executable to the workdir + async fn write_main_executable(&self) -> Result<()> { + match self.build.build.platform() { + // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder + // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable". + // + // The wasm stuff will be in a folder called "wasm" in the workdir. + // + // Final output format: + // ``` + // dx/ + // app/ + // web/ + // bundle/ + // build/ + // public/ + // index.html + // wasm/ + // app.wasm + // glue.js + // snippets/ + // ... + // assets/ + // logo.png + // ``` + Platform::Web => { + // Run wasm-bindgen and drop its output into the assets folder under "dioxus" + self.build.status_wasm_bindgen_start(); + self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"), &self.exe_dir()) + .await?; + + // Only run wasm-opt if the feature is enabled + // Wasm-opt has an expensive build script that makes it annoying to keep enabled for iterative dev + // We put it behind the "wasm-opt" feature flag so that it can be disabled when iterating on the cli + self.build.status_wasm_opt_start(); + self.run_wasm_opt(&self.exe_dir())?; + + // Write the index.html file with the pre-configured contents we got from pre-rendering + std::fs::write( + self.app_dir().join("index.html"), + self.build.prepare_html()?, + )?; + } + + // this will require some extra oomf to get the multi architecture builds... + // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1) + // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about + // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path + // + // todo(jon): maybe just symlink this rather than copy it? + Platform::Android => { + // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs + // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19 + std::fs::copy(&self.app.exe, self.main_exe())?; + } + + // These are all super simple, just copy the exe into the folder + // eventually, perhaps, maybe strip + encrypt the exe? + Platform::MacOS + | Platform::Windows + | Platform::Linux + | Platform::Ios + | Platform::Liveview + | Platform::Server => { + std::fs::copy(&self.app.exe, self.main_exe())?; + } + } + + Ok(()) + } + + /// Copy the assets out of the manifest and into the target location + /// + /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory + async fn write_assets(&self) -> Result<()> { + // Server doesn't need assets - web will provide them + if self.build.build.platform() == Platform::Server { + return Ok(()); + } + + let asset_dir = self.asset_dir(); + + // First, clear the asset dir + // todo(jon): cache the asset dir, removing old files and only copying new ones that changed since the last build + _ = std::fs::remove_dir_all(&asset_dir); + _ = create_dir_all(&asset_dir); + + // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't + let mut assets_to_transfer = vec![]; + + // Queue the bundled assets + for asset in self.app.assets.assets.keys() { + let bundled = self.app.assets.assets.get(asset).unwrap(); + let from = bundled.absolute.clone(); + let to = asset_dir.join(&bundled.bundled); + tracing::debug!("Copying asset {from:?} to {to:?}"); + assets_to_transfer.push((from, to)); + } + + // And then queue the legacy assets + // ideally, one day, we can just check the rsx!{} calls for references to assets + for from in self.build.krate.legacy_asset_dir_files() { + let to = asset_dir.join(from.file_name().unwrap()); + tracing::debug!("Copying legacy asset {from:?} to {to:?}"); + assets_to_transfer.push((from, to)); + } + + let asset_count = assets_to_transfer.len(); + let assets_finished = AtomicUsize::new(0); + + // Parallel Copy over the assets and keep track of progress with an atomic counter + // todo: we want to use the fastfs variant that knows how to parallelize folders, too + assets_to_transfer.par_iter().try_for_each(|(from, to)| { + self.build.status_copying_asset( + assets_finished.fetch_add(0, std::sync::atomic::Ordering::SeqCst), + asset_count, + from.clone(), + ); + + // todo(jon): implement optimize + pre_compress on the asset type + let res = crate::fastfs::copy_asset(from, to); + + if let Err(err) = res.as_ref() { + tracing::error!("Failed to copy asset {from:?}: {err}"); + } + + self.build.status_copying_asset( + assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1, + asset_count, + from.clone(), + ); + + res.map(|_| ()) + })?; + + Ok(()) + } + + /// The directory in which we'll put the main exe + /// + /// Mac, Android, Web are a little weird + /// - mac wants to be in Contents/MacOS + /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture) + /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work) + /// + /// I think all others are just in the root folder + /// + /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do. + pub fn exe_dir(&self) -> PathBuf { + match self.build.build.platform() { + Platform::MacOS => self.app_dir().join("Contents").join("MacOS"), + Platform::Android => self.app_dir().join("jniLibs").join("arm64-v8a"), + Platform::Web => self.app_dir().join("wasm"), + + // these are all the same, I think? + Platform::Windows + | Platform::Linux + | Platform::Ios + | Platform::Server + | Platform::Liveview => self.app_dir(), + } + } + + /// The item that we'll try to run directly if we need to. + /// + /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod + pub fn main_exe(&self) -> PathBuf { + // todo(jon): this could just be named `App` or the name of the app like `Raycast` in `Raycast.app` + match self.build.build.platform() { + Platform::MacOS => self.exe_dir().join("DioxusApp"), + Platform::Ios => self.exe_dir().join("DioxusApp"), + Platform::Server => self.exe_dir().join("server"), + Platform::Liveview => self.exe_dir().join("server"), + Platform::Windows => self.exe_dir().join("app.exe"), + Platform::Linux => self.exe_dir().join("AppRun"), // from the appimage spec, the root exe needs to be named `AppRun` + Platform::Android => self.exe_dir().join("libdioxusapp.so"), // from the apk spec, the root exe will actually be a shared library + Platform::Web => unimplemented!("there's no main exe on web"), // this will be wrong, I think, but not important? + } + } + + pub fn asset_dir(&self) -> PathBuf { + match self.build.build.platform() { + // macos why are you weird + Platform::MacOS => self + .app_dir() + .join("Contents") + .join("Resources") + .join("assets"), + + // everyone else is soooo normal, just app/assets :) + Platform::Web + | Platform::Ios + | Platform::Windows + | Platform::Linux + | Platform::Android + | Platform::Server + | Platform::Liveview => self.app_dir().join("assets"), + } + } + + /// We always put the server in the `web` folder! + /// Only the `web` target will generate a `public` folder though + async fn write_server_executable(&self) -> Result<()> { + if let Some(server) = &self.server { + let to = self + .server_exe() + .expect("server should be set if we're building a server"); + + std::fs::create_dir_all(self.server_exe().unwrap().parent().unwrap())?; + + tracing::debug!("Copying server executable from {server:?} to {to:?}"); + + // Remove the old server executable if it exists, since copying might corrupt it :( + // todo(jon): do this in more places, I think + _ = std::fs::remove_file(&to); + std::fs::copy(&server.exe, to)?; + } + + Ok(()) + } + + /// todo(jon): use handlebars templates instead of these prebaked templates + async fn write_metadata(&self) -> Result<()> { + // write the Info.plist file + match self.build.build.platform() { + Platform::MacOS => { + let src = include_str!("../../assets/macos/mac.plist"); + let dest = self.app_dir().join("Contents").join("Info.plist"); + std::fs::write(dest, src)?; + } + + Platform::Ios => { + let src = include_str!("../../assets/ios/ios.plist"); + let dest = self.app_dir().join("Info.plist"); + std::fs::write(dest, src)?; + } + + // AndroidManifest.xml + // er.... maybe even all the kotlin/java/gradle stuff? + Platform::Android => {} + + // Probably some custom format or a plist file (haha) + // When we do the proper bundle, we'll need to do something with wix templates, I think? + Platform::Windows => {} + + // eventually we'll create the .appimage file, I guess? + Platform::Linux => {} + + // These are served as folders, not appimages, so we don't need to do anything special (I think?) + // Eventually maybe write some secrets/.env files for the server? + // We could also distribute them as a deb/rpm for linux and msi for windows + Platform::Web => {} + Platform::Server => {} + Platform::Liveview => {} + } + + Ok(()) + } + + /// Run the optimizers, obfuscators, minimizers, signers, etc + pub(crate) async fn optimize(&self) -> Result<()> { + match self.build.build.platform() { + Platform::Web => { + // Compress the asset dir + // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output + let pre_compress = self + .build + .krate + .should_pre_compress_web_assets(self.build.build.release); + + let bindgen_dir = self.exe_dir(); + tokio::task::spawn_blocking(move || { + crate::fastfs::pre_compress_folder(&bindgen_dir, pre_compress) + }) + .await + .unwrap()?; + } + Platform::MacOS => {} + Platform::Windows => {} + Platform::Linux => {} + Platform::Ios => {} + Platform::Android => {} + Platform::Server => {} + Platform::Liveview => {} + } + + Ok(()) + } + + pub(crate) fn server_exe(&self) -> Option { + if let Some(_server) = &self.server { + return Some( + self.build + .krate + .build_dir(Platform::Server, self.build.build.release) + .join("server"), + ); + } + + None + } + + /// returns the path to .app/.apk/.appimage folder + /// + /// we only add an extension to the folders where it sorta matters that it's named with the extension. + /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc. + /// + /// for our simulator-based platforms, this is less important since they need to be zipped up anyways + /// to run in the simulator. + /// + /// For windows/linux, it's also not important since we're just running the exe directly out of the folder + pub(crate) fn app_dir(&self) -> PathBuf { + let platform_dir = self + .build + .krate + .build_dir(self.build.build.platform(), self.build.build.release); + + match self.build.build.platform() { + Platform::Web => platform_dir.join("public"), + Platform::Server => platform_dir.clone(), // ends up *next* to the public folder + + // These might not actually need to be called `.app` but it does let us run these with `open` + Platform::MacOS => platform_dir.join("DioxusApp.app"), + Platform::Ios => platform_dir.join("DioxusApp.app"), + + // in theory, these all could end up in the build dir + Platform::Linux => platform_dir.join("app"), // .appimage (after bundling) + Platform::Windows => platform_dir.join("app"), // .exe (after bundling) + Platform::Android => platform_dir.join("app"), // .apk (after bundling) + Platform::Liveview => platform_dir.join("app"), // .exe (after bundling) + } + } + + pub(crate) async fn run_wasm_bindgen( + &self, + input_path: &Path, + bindgen_outdir: &Path, + ) -> anyhow::Result<()> { + tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen"); + + let input_path = input_path.to_path_buf(); + let bindgen_outdir = bindgen_outdir.to_path_buf(); + let name = self.build.krate.executable_name().to_string(); + let keep_debug = self.build.krate.config.web.wasm_opt.debug || (!self.build.build.release); + + let start = std::time::Instant::now(); + tokio::task::spawn_blocking(move || { + Bindgen::new() + .input_path(&input_path) + .web(true) + .unwrap() + .debug(keep_debug) + .demangle(keep_debug) + .keep_debug(keep_debug) + .reference_types(true) + .remove_name_section(!keep_debug) + .remove_producers_section(!keep_debug) + .out_name(&name) + .generate(&bindgen_outdir) + }) + .await + .context("Wasm-bindgen crashed while optimizing the wasm binary")? + .context("Failed to generate wasm-bindgen bindings")?; + + tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed()); + + Ok(()) + } + + #[allow(unused)] + pub(crate) fn run_wasm_opt(&self, bindgen_outdir: &std::path::Path) -> Result<()> { + if !self.build.build.release { + return Ok(()); + }; + + self.build.status_optimizing_wasm(); + + #[cfg(feature = "optimizations")] + { + use crate::config::WasmOptLevel; + + tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt..."); + + let mut options = match self.build.krate.config.web.wasm_opt.level { + WasmOptLevel::Z => { + wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively() + } + WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(), + WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(), + WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(), + WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(), + WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(), + WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(), + }; + let wasm_file = + bindgen_outdir.join(format!("{}_bg.wasm", self.build.krate.executable_name())); + let old_size = wasm_file.metadata()?.len(); + options + // WASM bindgen relies on reference types + .enable_feature(wasm_opt::Feature::ReferenceTypes) + .debug_info(self.build.krate.config.web.wasm_opt.debug) + .run(&wasm_file, &wasm_file) + .map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?; + + let new_size = wasm_file.metadata()?.len(); + tracing::debug!( + dx_src = ?TraceSrc::Build, + "wasm-opt reduced WASM size from {} to {} ({:2}%)", + old_size, + new_size, + (new_size as f64 - old_size as f64) / old_size as f64 * 100.0 + ); + } + + Ok(()) + } +} diff --git a/packages/cli/src/builder/cargo.rs b/packages/cli/src/builder/cargo.rs deleted file mode 100644 index c8471994e..000000000 --- a/packages/cli/src/builder/cargo.rs +++ /dev/null @@ -1,272 +0,0 @@ -use super::web::install_web_build_tooling; -use super::BuildRequest; -use super::BuildResult; -use super::TargetPlatform; -use crate::assets::copy_dir_to; -use crate::assets::create_assets_head; -use crate::assets::{asset_manifest, process_assets, AssetConfigDropGuard}; -use crate::builder::progress::build_cargo; -use crate::builder::progress::CargoBuildResult; -use crate::builder::progress::Stage; -use crate::builder::progress::UpdateBuildProgress; -use crate::builder::progress::UpdateStage; -use crate::config::Platform; -use crate::link::LinkCommand; -use crate::Result; -use crate::TraceSrc; -use anyhow::Context; -use futures_channel::mpsc::UnboundedSender; -use manganis_cli_support::AssetManifest; -use manganis_cli_support::ManganisSupportGuard; -use std::fs::create_dir_all; -use std::path::PathBuf; -use tracing::error; - -impl BuildRequest { - /// Create a list of arguments for cargo builds - pub(crate) fn build_arguments(&self) -> Vec { - let mut cargo_args = Vec::new(); - - if self.build_arguments.release { - cargo_args.push("--release".to_string()); - } - if self.build_arguments.verbose { - cargo_args.push("--verbose".to_string()); - } else { - cargo_args.push("--quiet".to_string()); - } - - if let Some(custom_profile) = &self.build_arguments.profile { - cargo_args.push("--profile".to_string()); - cargo_args.push(custom_profile.to_string()); - } - - if !self.build_arguments.target_args.features.is_empty() { - let features_str = self.build_arguments.target_args.features.join(" "); - cargo_args.push("--features".to_string()); - cargo_args.push(features_str); - } - - if let Some(target) = self - .targeting_web() - .then_some("wasm32-unknown-unknown") - .or(self.build_arguments.target_args.target.as_deref()) - { - cargo_args.push("--target".to_string()); - cargo_args.push(target.to_string()); - } - - if let Some(ref platform) = self.build_arguments.target_args.package { - cargo_args.push(String::from("-p")); - cargo_args.push(platform.clone()); - } - - cargo_args.append(&mut self.build_arguments.cargo_args.clone()); - - match self.dioxus_crate.executable_type() { - krates::cm::TargetKind::Bin => { - cargo_args.push("--bin".to_string()); - } - krates::cm::TargetKind::Lib => { - cargo_args.push("--lib".to_string()); - } - krates::cm::TargetKind::Example => { - cargo_args.push("--example".to_string()); - } - _ => {} - }; - cargo_args.push(self.dioxus_crate.executable_name().to_string()); - - cargo_args - } - - /// Create a build command for cargo - fn prepare_build_command(&self) -> Result<(tokio::process::Command, Vec)> { - let mut cmd = tokio::process::Command::new("cargo"); - cmd.arg("rustc"); - if let Some(target_dir) = &self.target_dir { - cmd.env("CARGO_TARGET_DIR", target_dir); - } - cmd.current_dir(self.dioxus_crate.crate_dir()) - .arg("--message-format") - .arg("json-diagnostic-rendered-ansi"); - - let cargo_args = self.build_arguments(); - cmd.args(&cargo_args); - - cmd.arg("--").args(self.rust_flags.clone()); - - Ok((cmd, cargo_args)) - } - - pub(crate) async fn build( - &self, - mut progress: UnboundedSender, - ) -> Result { - tracing::info!( - dx_src = ?TraceSrc::Build, - "Running build [{}] command...", - self.target_platform, - ); - - // Set up runtime guards - let mut dioxus_version = crate::dx_build_info::PKG_VERSION.to_string(); - if let Some(hash) = crate::dx_build_info::GIT_COMMIT_HASH_SHORT { - let hash = &hash.trim_start_matches('g')[..4]; - dioxus_version.push_str(&format!("-{hash}")); - } - let _manganis_support = ManganisSupportGuard::default(); - let _asset_guard = - AssetConfigDropGuard::new(self.dioxus_crate.dioxus_config.web.app.base_path.as_deref()); - - // If this is a web, build make sure we have the web build tooling set up - if self.targeting_web() { - install_web_build_tooling(&mut progress).await?; - } - - // Create the build command - let (cmd, cargo_args) = self.prepare_build_command()?; - - // Run the build command with a pretty loader - let crate_count = self.get_unit_count_estimate().await; - let cargo_result = build_cargo(crate_count, cmd, &mut progress).await?; - - // Post process the build result - let build_result = self - .post_process_build(cargo_args, &cargo_result, &mut progress) - .await - .context("Failed to post process build")?; - - tracing::info!( - dx_src = ?TraceSrc::Build, - "Build completed: [{}]", - self.dioxus_crate.out_dir().display(), - ); - - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::Finished, - update: UpdateStage::Start, - }); - - Ok(build_result) - } - - async fn post_process_build( - &self, - cargo_args: Vec, - cargo_build_result: &CargoBuildResult, - progress: &mut UnboundedSender, - ) -> Result { - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::OptimizingAssets, - update: UpdateStage::Start, - }); - - let assets = self.collect_assets(cargo_args, progress).await?; - - let file_name = self.dioxus_crate.executable_name(); - - // Move the final output executable into the dist folder - let out_dir = self.target_out_dir(); - if !out_dir.is_dir() { - create_dir_all(&out_dir)?; - } - let mut output_path = out_dir.join(file_name); - if self.targeting_web() { - output_path.set_extension("wasm"); - } else if cfg!(windows) { - output_path.set_extension("exe"); - } - if let Some(res_path) = &cargo_build_result.output_location { - std::fs::copy(res_path, &output_path)?; - } - - self.copy_assets_dir()?; - - // Create the build result - let build_result = BuildResult { - executable: output_path, - target_platform: self.target_platform, - }; - - // If this is a web build, run web post processing steps - if self.targeting_web() { - self.post_process_web_build(&build_result, assets.as_ref(), progress) - .await?; - } - - Ok(build_result) - } - - async fn collect_assets( - &self, - cargo_args: Vec, - progress: &mut UnboundedSender, - ) -> anyhow::Result> { - // If this is the server build, the client build already copied any assets we need - if self.target_platform == TargetPlatform::Server { - return Ok(None); - } - // If assets are skipped, we don't need to collect them - if self.build_arguments.skip_assets { - return Ok(None); - } - - // Start Manganis linker intercept. - let linker_args = vec![format!("{}", self.target_out_dir().display())]; - - // Don't block the main thread - manganis should not be running its own std process but it's - // fine to wrap it here at the top - let build = self.clone(); - let mut progress = progress.clone(); - tokio::task::spawn_blocking(move || { - manganis_cli_support::start_linker_intercept( - &LinkCommand::command_name(), - cargo_args, - Some(linker_args), - )?; - let Some(assets) = asset_manifest(&build) else { - error!(dx_src = ?TraceSrc::Build, "the asset manifest was not provided by manganis and we were not able to collect assets"); - return Err(anyhow::anyhow!("asset manifest was not provided by manganis")); - }; - // Collect assets from the asset manifest the linker intercept created - process_assets(&build, &assets, &mut progress)?; - // Create the __assets_head.html file for bundling - create_assets_head(&build, &assets)?; - - Ok(Some(assets)) - }) - .await - .map_err(|e| anyhow::anyhow!(e))? - } - - pub fn copy_assets_dir(&self) -> anyhow::Result<()> { - tracing::info!(dx_src = ?TraceSrc::Build, "Copying public assets to the output directory..."); - let out_dir = self.target_out_dir(); - let asset_dir = self.dioxus_crate.asset_dir(); - - if asset_dir.is_dir() { - // Only pre-compress the assets from the web build. Desktop assets are not served, so they don't need to be pre_compressed - let pre_compress = self.targeting_web() - && self - .dioxus_crate - .should_pre_compress_web_assets(self.build_arguments.release); - - copy_dir_to(asset_dir, out_dir, pre_compress)?; - } - Ok(()) - } - - /// Get the output directory for a specific built target - pub fn target_out_dir(&self) -> PathBuf { - let out_dir = self.dioxus_crate.out_dir(); - match self.build_arguments.platform { - Some(Platform::Fullstack | Platform::StaticGeneration) => match self.target_platform { - TargetPlatform::Web => out_dir.join("public"), - TargetPlatform::Desktop => out_dir.join("desktop"), - _ => out_dir, - }, - _ => out_dir, - } - } -} diff --git a/packages/cli/src/builder/fullstack.rs b/packages/cli/src/builder/fullstack.rs deleted file mode 100644 index 3930ed99e..000000000 --- a/packages/cli/src/builder/fullstack.rs +++ /dev/null @@ -1,128 +0,0 @@ -use toml_edit::Item; - -use crate::builder::Build; -use crate::dioxus_crate::DioxusCrate; - -use crate::builder::BuildRequest; -use std::io::Write; - -use super::TargetPlatform; - -static CLIENT_PROFILE: &str = "dioxus-client"; -static SERVER_PROFILE: &str = "dioxus-server"; - -// The `opt-level=2` increases build times, but can noticeably decrease time -// between saving changes and being able to interact with an app. The "overall" -// time difference (between having and not having the optimization) can be -// almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends -// on setup (hardware, OS, browser, idle load). -// Find or create the client and server profiles in the .cargo/config.toml file -fn initialize_profiles(config: &DioxusCrate) -> crate::Result<()> { - let config_path = config.workspace_dir().join(".cargo/config.toml"); - let mut config = match std::fs::read_to_string(&config_path) { - Ok(config) => config.parse::().map_err(|e| { - crate::Error::Other(anyhow::anyhow!("Failed to parse .cargo/config.toml: {}", e)) - })?, - Err(_) => Default::default(), - }; - - if let Item::Table(table) = config - .as_table_mut() - .entry("profile") - .or_insert(Item::Table(Default::default())) - { - if let toml_edit::Entry::Vacant(entry) = table.entry(CLIENT_PROFILE) { - let mut client = toml_edit::Table::new(); - client.insert("inherits", Item::Value("dev".into())); - client.insert("opt-level", Item::Value(2.into())); - entry.insert(Item::Table(client)); - } - - if let toml_edit::Entry::Vacant(entry) = table.entry(SERVER_PROFILE) { - let mut server = toml_edit::Table::new(); - server.insert("inherits", Item::Value("dev".into())); - server.insert("opt-level", Item::Value(2.into())); - entry.insert(Item::Table(server)); - } - } - - // Write the config back to the file - if let Some(parent) = config_path.parent() { - std::fs::create_dir_all(parent)?; - } - let file = std::fs::File::create(config_path)?; - let mut buf_writer = std::io::BufWriter::new(file); - write!(buf_writer, "{}", config)?; - - Ok(()) -} - -impl BuildRequest { - pub(crate) fn new_fullstack( - config: DioxusCrate, - build_arguments: Build, - serve: bool, - ) -> Result, crate::Error> { - initialize_profiles(&config)?; - - Ok(vec![ - Self::new_client(serve, &config, &build_arguments), - Self::new_server(serve, &config, &build_arguments), - ]) - } - - fn new_with_target_directory_rust_flags_and_features( - serve: bool, - config: &DioxusCrate, - build: &Build, - feature: Option, - target_platform: TargetPlatform, - ) -> Self { - let config = config.clone(); - let mut build = build.clone(); - // Add the server feature to the features we pass to the build - if let Some(feature) = feature { - build.target_args.features.push(feature); - } - - // Add the server flags to the build arguments - Self { - serve, - build_arguments: build.clone(), - dioxus_crate: config, - rust_flags: Default::default(), - target_dir: None, - target_platform, - } - } - - fn new_server(serve: bool, config: &DioxusCrate, build: &Build) -> Self { - let mut build = build.clone(); - if !build.release && build.profile.is_none() { - build.profile = Some(CLIENT_PROFILE.to_string()); - } - let client_feature = build.auto_detect_server_feature(config); - Self::new_with_target_directory_rust_flags_and_features( - serve, - config, - &build, - build.target_args.server_feature.clone().or(client_feature), - TargetPlatform::Server, - ) - } - - fn new_client(serve: bool, config: &DioxusCrate, build: &Build) -> Self { - let mut build = build.clone(); - if !build.release && build.profile.is_none() { - build.profile = Some(SERVER_PROFILE.to_string()); - } - let (client_feature, client_platform) = build.auto_detect_client_platform(config); - Self::new_with_target_directory_rust_flags_and_features( - serve, - config, - &build, - build.target_args.client_feature.clone().or(client_feature), - client_platform, - ) - } -} diff --git a/packages/cli/src/builder/mod.rs b/packages/cli/src/builder/mod.rs index 24f145886..5808f6210 100644 --- a/packages/cli/src/builder/mod.rs +++ b/packages/cli/src/builder/mod.rs @@ -1,272 +1,18 @@ -use crate::cli::serve::ServeArguments; -use crate::config::Platform; -use crate::dioxus_crate::DioxusCrate; -use crate::Result; -use crate::{build::Build, TraceSrc}; -use futures_util::stream::select_all; -use futures_util::StreamExt; -use std::net::SocketAddr; -use std::str::FromStr; -use std::{path::PathBuf, process::Stdio}; -use tokio::process::{Child, Command}; +//! The primary entrypoint for our build + optimize + bundle engine +//! +//! Handles multiple ongoing tasks and allows you to queue up builds from interactive and non-interactive contexts +//! +//! Uses a request -> response architecture that allows you to monitor the progress with an optional message +//! receiver. -mod cargo; -mod fullstack; -mod prepare_html; +mod build; +mod bundle; mod progress; +mod runner; +mod verify; mod web; -pub use progress::{Stage, UpdateBuildProgress, UpdateStage}; -/// The target platform for the build -/// This is very similar to the Platform enum, but we need to be able to differentiate between the -/// server and web targets for the fullstack platform -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TargetPlatform { - Web, - Desktop, - Server, - Liveview, -} - -impl FromStr for TargetPlatform { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "web" => Ok(Self::Web), - "desktop" => Ok(Self::Desktop), - "axum" | "server" => Ok(Self::Server), - "liveview" => Ok(Self::Liveview), - _ => Err(()), - } - } -} - -impl std::fmt::Display for TargetPlatform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TargetPlatform::Web => write!(f, "web"), - TargetPlatform::Desktop => write!(f, "desktop"), - TargetPlatform::Server => write!(f, "server"), - TargetPlatform::Liveview => write!(f, "liveview"), - } - } -} - -/// A request for a project to be built -#[derive(Clone)] -pub struct BuildRequest { - /// Whether the build is for serving the application - pub serve: bool, - /// The configuration for the crate we are building - pub dioxus_crate: DioxusCrate, - /// The target platform for the build - pub target_platform: TargetPlatform, - /// The arguments for the build - pub build_arguments: Build, - /// The rustc flags to pass to the build - pub rust_flags: Vec, - /// The target directory for the build - pub target_dir: Option, -} - -impl BuildRequest { - pub fn create( - serve: bool, - dioxus_crate: &DioxusCrate, - build_arguments: impl Into, - ) -> crate::Result> { - let build_arguments = build_arguments.into(); - let platform = build_arguments.platform(); - let single_platform = |platform| { - let dioxus_crate = dioxus_crate.clone(); - vec![Self { - serve, - dioxus_crate, - build_arguments: build_arguments.clone(), - target_platform: platform, - rust_flags: Default::default(), - target_dir: Default::default(), - }] - }; - Ok(match platform { - Platform::Liveview => single_platform(TargetPlatform::Liveview), - Platform::Web => single_platform(TargetPlatform::Web), - Platform::Desktop => single_platform(TargetPlatform::Desktop), - Platform::StaticGeneration | Platform::Fullstack => { - Self::new_fullstack(dioxus_crate.clone(), build_arguments, serve)? - } - }) - } - - pub(crate) async fn build_all_parallel( - build_requests: Vec, - ) -> Result> { - let multi_platform_build = build_requests.len() > 1; - let mut build_progress = Vec::new(); - let mut set = tokio::task::JoinSet::new(); - for build_request in build_requests { - let (tx, rx) = futures_channel::mpsc::unbounded(); - build_progress.push((build_request.build_arguments.platform(), rx)); - set.spawn(async move { build_request.build(tx).await }); - } - - // Watch the build progress as it comes in - loop { - let mut next = select_all( - build_progress - .iter_mut() - .map(|(platform, rx)| rx.map(move |update| (*platform, update))), - ); - match next.next().await { - Some((platform, update)) => { - if multi_platform_build { - print!("{platform} build: "); - update.to_std_out(); - } else { - update.to_std_out(); - } - } - None => { - break; - } - } - } - - let mut all_results = Vec::new(); - - while let Some(result) = set.join_next().await { - let result = result - .map_err(|_| crate::Error::Unique("Failed to build project".to_owned()))??; - all_results.push(result); - } - - Ok(all_results) - } - - /// Check if the build is targeting the web platform - pub fn targeting_web(&self) -> bool { - self.target_platform == TargetPlatform::Web - } -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct OpenArguments { - fullstack_address: Option, - devserver_addr: Option, - always_on_top: Option, - workspace: PathBuf, - asset_root: PathBuf, - app_title: String, - out_dir: PathBuf, - serve: bool, -} - -impl OpenArguments { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - serve: &ServeArguments, - fullstack_address: Option, - dioxus_crate: &DioxusCrate, - ) -> Self { - Self { - devserver_addr: Some(serve.address.address()), - always_on_top: Some(serve.always_on_top.unwrap_or(true)), - serve: true, - fullstack_address, - workspace: dioxus_crate.workspace_dir().to_path_buf(), - asset_root: dioxus_crate.asset_dir().to_path_buf(), - app_title: dioxus_crate.dioxus_config.application.name.clone(), - out_dir: dioxus_crate.out_dir().to_path_buf(), - } - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn new_for_static_generation_build(dioxus_crate: &DioxusCrate) -> Self { - Self { - workspace: dioxus_crate.workspace_dir().to_path_buf(), - asset_root: dioxus_crate.asset_dir().to_path_buf(), - app_title: dioxus_crate.dioxus_config.application.name.clone(), - out_dir: dioxus_crate.out_dir().to_path_buf(), - ..Default::default() - } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct BuildResult { - pub executable: PathBuf, - pub target_platform: TargetPlatform, -} - -impl BuildResult { - /// Open the executable if this is a native build - pub fn open(&self, arguments: OpenArguments) -> std::io::Result> { - match self.target_platform { - TargetPlatform::Web => { - if let Some(address) = arguments.fullstack_address { - tracing::info!(dx_src = ?TraceSrc::Dev, "Serving web app on http://{} 🎉", address); - } - return Ok(None); - } - TargetPlatform::Desktop => { - tracing::info!(dx_src = ?TraceSrc::Dev, "Launching desktop app at {} 🎉", self.executable.display()); - } - TargetPlatform::Server => { - // shut this up for now - the web app will take priority in logging - } - TargetPlatform::Liveview => { - if let Some(fullstack_address) = arguments.fullstack_address { - tracing::info!( - dx_src = ?TraceSrc::Dev, - "Launching liveview server on http://{:?} 🎉", - fullstack_address - ); - } - } - } - - if arguments.serve { - tracing::info!(dx_src = ?TraceSrc::Dev, "Press [o] to open the app manually."); - } - - let executable = self.executable.canonicalize()?; - let mut cmd = Command::new(executable); - - // Set the env vars that the clients will expect - // These need to be stable within a release version (ie 0.6.0) - cmd.env(dioxus_cli_config::CLI_ENABLED_ENV, "true"); - if let Some(addr) = arguments.fullstack_address { - cmd.env(dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string()); - cmd.env(dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()); - } - if let Some(always_on_top) = arguments.always_on_top { - cmd.env( - dioxus_cli_config::ALWAYS_ON_TOP_ENV, - always_on_top.to_string(), - ); - } - cmd.env( - dioxus_cli_config::ASSET_ROOT_ENV, - arguments.asset_root.display().to_string(), - ); - if let Some(devserver_addr) = arguments.devserver_addr { - cmd.env( - dioxus_cli_config::DEVSERVER_RAW_ADDR_ENV, - devserver_addr.to_string(), - ); - } - cmd.env(dioxus_cli_config::APP_TITLE_ENV, arguments.app_title); - cmd.env( - dioxus_cli_config::OUT_DIR, - arguments.out_dir.display().to_string(), - ); - - cmd.stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .current_dir(arguments.workspace); - - Ok(Some(cmd.spawn()?)) - } -} +pub(crate) use build::*; +pub(crate) use bundle::*; +pub(crate) use progress::*; +pub(crate) use runner::*; diff --git a/packages/cli/src/builder/prepare_html.rs b/packages/cli/src/builder/prepare_html.rs deleted file mode 100644 index fedfa0a2f..000000000 --- a/packages/cli/src/builder/prepare_html.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Build the HTML file to load a web application. The index.html file may be created from scratch or modified from the `index.html` file in the crate root. - -use super::{BuildRequest, UpdateBuildProgress}; -use crate::Result; -use crate::TraceSrc; -use futures_channel::mpsc::UnboundedSender; -use manganis_cli_support::AssetManifest; -use std::fmt::Write; -use std::path::{Path, PathBuf}; - -const DEFAULT_HTML: &str = include_str!("../../assets/index.html"); -const TOAST_HTML: &str = include_str!("../../assets/toast.html"); - -impl BuildRequest { - pub(crate) fn prepare_html( - &self, - assets: Option<&AssetManifest>, - _progress: &mut UnboundedSender, - ) -> Result { - let mut html = html_or_default(&self.dioxus_crate.crate_dir()); - - // Inject any resources from the config into the html - self.inject_resources(&mut html, assets)?; - - // Inject loading scripts if they are not already present - self.inject_loading_scripts(&mut html); - - // Replace any special placeholders in the HTML with resolved values - self.replace_template_placeholders(&mut html); - - let title = self.dioxus_crate.dioxus_config.web.app.title.clone(); - - replace_or_insert_before("{app_title}", ") -> Result<()> { - // Collect all resources into a list of styles and scripts - let resources = &self.dioxus_crate.dioxus_config.web.resource; - let mut style_list = resources.style.clone().unwrap_or_default(); - let mut script_list = resources.script.clone().unwrap_or_default(); - - if self.serve { - style_list.extend(resources.dev.style.iter().cloned()); - script_list.extend(resources.dev.script.iter().cloned()); - } - - let mut head_resources = String::new(); - // Add all styles to the head - for style in &style_list { - writeln!( - &mut head_resources, - "", - &style.to_str().unwrap(), - )?; - } - - if !style_list.is_empty() { - self.send_resource_deprecation_warning(style_list, ResourceType::Style); - } - - // Add all scripts to the head - for script in &script_list { - writeln!( - &mut head_resources, - "", - &script.to_str().unwrap(), - )?; - } - - if !script_list.is_empty() { - self.send_resource_deprecation_warning(script_list, ResourceType::Script); - } - - // Inject any resources from manganis into the head - if let Some(assets) = assets { - head_resources.push_str(&assets.head()); - } - - replace_or_insert_before("{style_include}", " - // We can't use a module script here because we need to start the script immediately when streaming - import("/{base_path}/assets/dioxus/{app_name}.js").then( - ({ default: init }) => { - init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then((wasm) => { - if (wasm.__wbindgen_start == undefined) { - wasm.main(); - } - }); - } - ); - - {DX_TOAST_UTILITIES} - html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML), - false => html.replace("{DX_TOAST_UTILITIES}", ""), - }; - - // And try to insert preload links for the wasm and js files - *html = html.replace( - " - - , variant: ResourceType) { - const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release."#; - - let replacement_components = paths - .iter() - .map(|path| { - let path = if path.exists() { - path.to_path_buf() - } else { - // If the path is absolute, make it relative to the current directory before we join it - // The path is actually a web path which is relative to the root of the website - let path = path.strip_prefix("/").unwrap_or(path); - let asset_dir_path = self.dioxus_crate.asset_dir().join(path); - if let Ok(absolute_path) = asset_dir_path.canonicalize() { - let absolute_crate_root = - self.dioxus_crate.crate_dir().canonicalize().unwrap(); - PathBuf::from("./") - .join(absolute_path.strip_prefix(absolute_crate_root).unwrap()) - } else { - path.to_path_buf() - } - }; - match variant { - ResourceType::Style => format!( - " document::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}", - path.display() - ), - ResourceType::Script => { - format!(" Script {{ src: asset!(file(\"{}\")) }}", path.display()) - } - } - }) - .collect::>(); - let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n")); - let section_name = match variant { - ResourceType::Style => "web.resource.style", - ResourceType::Script => "web.resource.script", - }; - - let message = format!( - "{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```" - ); - - tracing::warn!(dx_src = ?TraceSrc::Build, "{}", message); - } -} - -enum ResourceType { - Style, - Script, -} - -/// Read the html file from the crate root or use the default html file -fn html_or_default(crate_root: &Path) -> String { - let custom_html_file = crate_root.join("index.html"); - std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML)) -} - -/// Replace a string or insert the new contents before a marker -fn replace_or_insert_before( - replace: &str, - or_insert_before: &str, - with: &str, - content: &mut String, -) { - if content.contains(replace) { - *content = content.replace(replace, with); - } else if let Some(pos) = content.find(or_insert_before) { - content.insert_str(pos, with); - } -} diff --git a/packages/cli/src/builder/progress.rs b/packages/cli/src/builder/progress.rs index d4966859b..769c67de6 100644 --- a/packages/cli/src/builder/progress.rs +++ b/packages/cli/src/builder/progress.rs @@ -1,225 +1,124 @@ //! Report progress about the build to the user. We use channels to report progress back to the CLI. -use crate::TraceSrc; - -use super::BuildRequest; -use anyhow::Context; -use cargo_metadata::Message; -use futures_channel::mpsc::UnboundedSender; -use serde::Deserialize; -use std::ops::Deref; +use crate::{AppBundle, BuildRequest, Platform, TraceSrc}; +use cargo_metadata::CompilerMessage; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::path::PathBuf; -use std::process::Stdio; -use tokio::io::AsyncBufReadExt; -#[derive(Default, Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)] -pub enum Stage { - #[default] - Initializing = 0, - InstallingWasmTooling = 1, - Compiling = 2, - OptimizingWasm = 3, - OptimizingAssets = 4, - Finished = 5, +pub(crate) type ProgressTx = UnboundedSender; +pub(crate) type ProgressRx = UnboundedReceiver; + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum BuildUpdate { + Progress { stage: BuildStage }, + CompilerMessage { message: CompilerMessage }, + BuildReady { bundle: AppBundle }, + BuildFailed { err: crate::Error }, } -impl Deref for Stage { - type Target = str; - - fn deref(&self) -> &Self::Target { - match self { - Stage::Initializing => "Initializing", - Stage::InstallingWasmTooling => "Installing Wasm Tooling", - Stage::Compiling => "Compiling", - Stage::OptimizingWasm => "Optimizing Wasm", - Stage::OptimizingAssets => "Optimizing Assets", - Stage::Finished => "Finished", - } - } -} - -impl std::fmt::Display for Stage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.deref()) - } -} - -#[derive(Debug, Clone)] -pub struct UpdateBuildProgress { - pub stage: Stage, - pub update: UpdateStage, -} - -impl UpdateBuildProgress { - pub fn to_std_out(&self) { - match &self.update { - UpdateStage::Start => println!("--- {} ---", self.stage), - UpdateStage::SetProgress(progress) => { - println!("Build progress {:0.0}%", progress * 100.0); - } - UpdateStage::Failed(message) => { - println!("Build failed: {}", message); - } - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum UpdateStage { - Start, - SetProgress(f64), - Failed(String), -} - -pub(crate) async fn build_cargo( - crate_count: usize, - mut cmd: tokio::process::Command, - progress: &mut UnboundedSender, -) -> anyhow::Result { - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::Compiling, - update: UpdateStage::Start, - }); - - let mut child = cmd - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("Failed to spawn cargo build")?; - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - let stdout = tokio::io::BufReader::new(stdout); - let stderr = tokio::io::BufReader::new(stderr); - let mut output_location = None; - - let mut stdout = stdout.lines(); - let mut stderr = stderr.lines(); - let mut units_compiled = 0; - let mut errors = Vec::new(); - loop { - let line = tokio::select! { - line = stdout.next_line() => { - line - } - line = stderr.next_line() => { - line - } - }; - let Some(line) = line? else { - break; - }; - let mut deserializer = serde_json::Deserializer::from_str(line.trim()); - deserializer.disable_recursion_limit(); - - let message = Message::deserialize(&mut deserializer).unwrap_or(Message::TextLine(line)); - match message { - Message::CompilerMessage(msg) => { - let message = msg.message; - tracing::info!(dx_src = ?TraceSrc::Cargo, dx_no_fmt = true, "{}", message.to_string()); - - const WARNING_LEVELS: &[cargo_metadata::diagnostic::DiagnosticLevel] = &[ - cargo_metadata::diagnostic::DiagnosticLevel::Help, - cargo_metadata::diagnostic::DiagnosticLevel::Note, - cargo_metadata::diagnostic::DiagnosticLevel::Warning, - cargo_metadata::diagnostic::DiagnosticLevel::Error, - cargo_metadata::diagnostic::DiagnosticLevel::FailureNote, - cargo_metadata::diagnostic::DiagnosticLevel::Ice, - ]; - const FATAL_LEVELS: &[cargo_metadata::diagnostic::DiagnosticLevel] = &[ - cargo_metadata::diagnostic::DiagnosticLevel::Error, - cargo_metadata::diagnostic::DiagnosticLevel::FailureNote, - cargo_metadata::diagnostic::DiagnosticLevel::Ice, - ]; - if WARNING_LEVELS.contains(&message.level) { - if let Some(rendered) = message.rendered { - errors.push(rendered); - } - } - if FATAL_LEVELS.contains(&message.level) { - return Err(anyhow::anyhow!(errors.join("\n"))); - } - } - Message::CompilerArtifact(artifact) => { - units_compiled += 1; - if let Some(executable) = artifact.executable { - output_location = Some(executable.into()); - } else { - let build_progress = units_compiled as f64 / crate_count as f64; - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::Compiling, - update: UpdateStage::SetProgress((build_progress).clamp(0.0, 1.00)), - }); - } - } - Message::BuildScriptExecuted(_) => { - units_compiled += 1; - } - Message::BuildFinished(finished) => { - if !finished.success { - return Err(anyhow::anyhow!("Build failed")); - } - } - Message::TextLine(line) => { - tracing::info!(dx_src = ?TraceSrc::Cargo, dx_no_fmt = true, "{}", line); - } - _ => { - // Unknown message - } - } - } - - Ok(CargoBuildResult { output_location }) -} - -pub(crate) struct CargoBuildResult { - pub(crate) output_location: Option, +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum BuildStage { + Initializing, + Starting { + platform: Platform, + crate_count: usize, + }, + InstallingTooling {}, + Compiling { + platform: Platform, + current: usize, + total: usize, + krate: String, + }, + Bundling {}, + RunningBindgen {}, + OptimizingWasm {}, + CopyingAssets { + current: usize, + total: usize, + path: PathBuf, + }, + Success, + Failed, + Aborted, + Restarting, } impl BuildRequest { - /// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed. - async fn get_unit_count(&self) -> Option { - #[derive(Debug, Deserialize)] - struct UnitGraph { - units: Vec, - } - - let mut cmd = tokio::process::Command::new("cargo"); - cmd.arg("+nightly"); - cmd.arg("build"); - cmd.arg("--unit-graph"); - cmd.arg("-Z").arg("unstable-options"); - - cmd.args(self.build_arguments()); - - let output = cmd - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await - .ok()?; - if !output.status.success() { - return None; - } - - let output_text = String::from_utf8(output.stdout).ok()?; - let graph: UnitGraph = serde_json::from_str(&output_text).ok()?; - - Some(graph.units.len()) + pub(crate) fn status_wasm_bindgen_start(&self) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::RunningBindgen {}, + }); + } + pub(crate) fn status_wasm_opt_start(&self) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::RunningBindgen {}, + }); } - /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata. - /// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable - pub(crate) async fn get_unit_count_estimate(&self) -> usize { - // Try to get it from nightly - self.get_unit_count().await.unwrap_or_else(|| { - // Otherwise, use cargo metadata - (self - .dioxus_crate - .krates - .krates_filtered(krates::DepKind::Dev) - .iter() - .map(|k| k.targets.len()) - .sum::() as f64 - / 3.5) as usize - }) + pub(crate) fn status_start_bundle(&self) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::Bundling {}, + }); + } + + pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) { + _ = self + .progress + .unbounded_send(BuildUpdate::CompilerMessage { message }); + } + + pub(crate) fn status_build_message(&self, line: String) { + tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}"); + } + + pub(crate) fn status_build_progress( + &self, + count: usize, + total: usize, + name: String, + platform: Platform, + ) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::Compiling { + current: count, + total, + krate: name, + platform, + }, + }); + } + + pub(crate) fn status_starting_build(&self, crate_count: usize) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::Starting { + platform: self.build.platform(), + crate_count, + }, + }); + } + + pub(crate) fn status_copying_asset(&self, current: usize, total: usize, path: PathBuf) { + tracing::trace!("Status copying asset {current}/{total} from {path:?}"); + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::CopyingAssets { + current, + total, + path, + }, + }); + } + + pub(crate) fn status_optimizing_wasm(&self) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::OptimizingWasm {}, + }); + } + + pub(crate) fn status_installing_tooling(&self) { + _ = self.progress.unbounded_send(BuildUpdate::Progress { + stage: BuildStage::InstallingTooling {}, + }); } } diff --git a/packages/cli/src/builder/runner.rs b/packages/cli/src/builder/runner.rs new file mode 100644 index 000000000..e4f18b605 --- /dev/null +++ b/packages/cli/src/builder/runner.rs @@ -0,0 +1,287 @@ +use crate::{ + AppBundle, BuildArgs, BuildRequest, BuildStage, BuildUpdate, DioxusCrate, Platform, ProgressRx, + ProgressTx, Result, +}; +use std::time::{Duration, Instant}; + +/// The component of the serve engine that watches ongoing builds and manages their state, handle, +/// and progress. +/// +/// Previously, the builder allowed multiple apps to be built simultaneously, but this newer design +/// simplifies the code and allows only one app and its server to be built at a time. +/// +/// Here, we track the number of crates being compiled, assets copied, the times of these events, and +/// other metadata that gives us useful indicators for the UI. +pub(crate) struct Builder { + // Components of the build + pub krate: DioxusCrate, + pub request: BuildRequest, + pub build: tokio::task::JoinHandle>, + pub tx: ProgressTx, + pub rx: ProgressRx, + + // Metadata about the build that needs to be managed by watching build updates + // used to render the TUI + pub stage: BuildStage, + pub compiled_crates: usize, + pub compiled_crates_server: usize, + pub expected_crates: usize, + pub expected_crates_server: usize, + pub bundling_progress: f64, + pub compile_start: Option, + pub compile_end: Option, + pub compile_end_server: Option, + pub bundle_start: Option, + pub bundle_end: Option, +} + +impl Builder { + /// Create a new builder and immediately start a build + pub(crate) fn start(krate: &DioxusCrate, args: BuildArgs) -> Result { + let (tx, rx) = futures_channel::mpsc::unbounded(); + let request = BuildRequest::new(krate.clone(), args, tx.clone()); + Ok(Self { + krate: krate.clone(), + request: request.clone(), + stage: BuildStage::Initializing, + build: tokio::spawn(async move { + // On the first build, we want to verify the tooling + // We wont bother verifying on subsequent builds + request.verify_tooling().await?; + + let res = request.build_all().await; + + // The first launch gets some extra logging :) + if res.is_ok() { + tracing::info!("Build completed successfully, launching app! 💫") + } + + res + }), + tx, + rx, + compiled_crates: 0, + expected_crates: 1, + expected_crates_server: 1, + compiled_crates_server: 0, + bundling_progress: 0.0, + compile_start: Some(Instant::now()), + compile_end: None, + compile_end_server: None, + bundle_start: None, + bundle_end: None, + }) + } + + /// Wait for any new updates to the builder - either it completed or gave us a message etc + pub(crate) async fn wait(&mut self) -> BuildUpdate { + use futures_util::StreamExt; + + // Wait for the build to finish or for it to emit a status message + let update = tokio::select! { + Some(progress) = self.rx.next() => progress, + bundle = (&mut self.build) => { + // Replace the build with an infinitely pending task so we can select it again without worrying about deadlocks/spins + self.build = tokio::task::spawn(std::future::pending()); + match bundle { + Ok(Ok(bundle)) => BuildUpdate::BuildReady { bundle }, + Ok(Err(err)) => BuildUpdate::BuildFailed { err }, + Err(err) => BuildUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:?}", err)) }, + } + }, + }; + + tracing::trace!("Build update: {update:?}"); + + // Update the internal stage of the build so the UI can render it + match &update { + BuildUpdate::Progress { stage } => { + // Prevent updates from flowing in after the build has already finished + if !self.is_finished() { + self.stage = stage.clone(); + } + + match stage { + BuildStage::Initializing => { + self.compiled_crates = 0; + self.compiled_crates_server = 0; + self.bundling_progress = 0.0; + } + BuildStage::Starting { + crate_count, + platform, + } => { + if *platform == Platform::Server { + self.expected_crates_server = *crate_count; + } else { + self.expected_crates = *crate_count; + } + } + BuildStage::InstallingTooling {} => {} + BuildStage::Compiling { + current, + total, + platform, + .. + } => { + if *platform == Platform::Server { + self.compiled_crates_server = *current; + self.expected_crates_server = *total; + } else { + self.compiled_crates = *current; + self.expected_crates = *total; + } + + if self.compile_start.is_none() { + self.compile_start = Some(Instant::now()); + } + } + BuildStage::Bundling {} => { + self.complete_compile(); + self.bundling_progress = 0.0; + self.bundle_start = Some(Instant::now()); + } + BuildStage::OptimizingWasm {} => {} + BuildStage::CopyingAssets { current, total, .. } => { + self.bundling_progress = *current as f64 / *total as f64; + } + BuildStage::Success => { + self.compiled_crates = self.expected_crates; + self.compiled_crates_server = self.expected_crates_server; + self.bundling_progress = 1.0; + } + BuildStage::Failed => { + self.compiled_crates = self.expected_crates; + self.compiled_crates_server = self.expected_crates_server; + self.bundling_progress = 1.0; + } + BuildStage::Aborted => {} + BuildStage::Restarting => { + self.compiled_crates = 0; + self.compiled_crates_server = 0; + self.expected_crates = 1; + self.bundling_progress = 0.0; + } + BuildStage::RunningBindgen {} => { + self.bundling_progress = 0.5; + } + } + } + BuildUpdate::CompilerMessage { .. } => {} + BuildUpdate::BuildReady { .. } => { + self.compiled_crates = self.expected_crates; + self.compiled_crates_server = self.expected_crates_server; + self.bundling_progress = 1.0; + self.stage = BuildStage::Success; + + self.complete_compile(); + self.bundle_end = Some(Instant::now()); + } + BuildUpdate::BuildFailed { .. } => { + tracing::debug!("Setting builder to failed state"); + self.stage = BuildStage::Failed; + } + } + + update + } + + /// Restart this builder with new build arguments. + pub(crate) fn rebuild(&mut self, args: BuildArgs) { + // Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit + self.abort_all(); + + // And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task + let request = BuildRequest::new(self.krate.clone(), args, self.tx.clone()); + self.request = request.clone(); + self.stage = BuildStage::Restarting; + + // This build doesn't have any extra special logging - rebuilds would get pretty noisy + self.build = tokio::spawn(async move { request.build_all().await }); + } + + /// Shutdown the current build process + /// + /// todo: might want to use a cancellation token here to allow cleaner shutdowns + pub(crate) fn abort_all(&mut self) { + self.build.abort(); + self.stage = BuildStage::Aborted; + self.compiled_crates = 0; + self.compiled_crates_server = 0; + self.expected_crates = 1; + self.bundling_progress = 0.0; + self.compile_start = None; + self.bundle_start = None; + self.bundle_end = None; + self.compile_end = None; + } + + /// Wait for the build to finish, returning the final bundle + /// Should only be used by code that's not interested in the intermediate updates and only cares about the final bundle + /// + /// todo(jon): maybe we want to do some logging here? The build/bundle/run screens could be made to + /// use the TUI output for prettier outputs. + pub(crate) async fn finish(&mut self) -> Result { + loop { + match self.wait().await { + BuildUpdate::BuildReady { bundle } => return Ok(bundle), + BuildUpdate::BuildFailed { err } => return Err(err), + BuildUpdate::Progress { .. } => {} + BuildUpdate::CompilerMessage { .. } => {} + } + } + } + + fn complete_compile(&mut self) { + if self.compile_end.is_none() { + self.compiled_crates = self.expected_crates; + self.compile_end = Some(Instant::now()); + self.compile_end_server = Some(Instant::now()); + } + } + + /// Get the total duration of the build, if all stages have completed + pub(crate) fn total_build_time(&self) -> Option { + Some(self.compile_duration()? + self.bundle_duration()?) + } + + pub(crate) fn compile_duration(&self) -> Option { + Some( + self.compile_end + .unwrap_or_else(Instant::now) + .duration_since(self.compile_start?), + ) + } + + pub(crate) fn bundle_duration(&self) -> Option { + Some( + self.bundle_end + .unwrap_or_else(Instant::now) + .duration_since(self.bundle_start?), + ) + } + + /// Return a number between 0 and 1 representing the progress of the app build + pub(crate) fn compile_progress(&self) -> f64 { + self.compiled_crates as f64 / self.expected_crates as f64 + } + + /// Return a number between 0 and 1 representing the progress of the server build + pub(crate) fn server_compile_progress(&self) -> f64 { + self.compiled_crates_server as f64 / self.expected_crates_server as f64 + } + + pub(crate) fn bundle_progress(&self) -> f64 { + self.bundling_progress + } + + fn is_finished(&self) -> bool { + match self.stage { + BuildStage::Success => true, + BuildStage::Failed => true, + BuildStage::Aborted => true, + BuildStage::Restarting => false, + _ => false, + } + } +} diff --git a/packages/cli/src/builder/verify.rs b/packages/cli/src/builder/verify.rs new file mode 100644 index 000000000..c25840fb8 --- /dev/null +++ b/packages/cli/src/builder/verify.rs @@ -0,0 +1,148 @@ +use std::process::Stdio; + +use crate::{BuildRequest, Platform, Result, RustupShow}; +use anyhow::Context; +use tokio::process::Command; + +impl BuildRequest { + /// Install any tooling that might be required for this build. + /// + /// This should generally be only called on the first build since it takes time to verify the tooling + /// is in place, and we don't want to slow down subsequent builds. + pub(crate) async fn verify_tooling(&self) -> Result<()> { + tracing::debug!("Verifying tooling..."); + self.status_installing_tooling(); + + self.krate + .initialize_profiles() + .context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?; + + let rustup = match RustupShow::from_cli().await { + Ok(out) => out, + Err(err) => { + tracing::error!("Failed to verify tooling: {err}\ndx will proceed, but you might run into errors later."); + return Ok(()); + } + }; + + match self.build.platform() { + Platform::Web => self.verify_web_tooling(rustup).await?, + Platform::Ios => self.verify_ios_tooling(rustup).await?, + Platform::Android => self.verify_android_tooling(rustup).await?, + Platform::Linux => self.verify_linux_tooling(rustup).await?, + Platform::MacOS => {} + Platform::Windows => {} + Platform::Server => {} + Platform::Liveview => {} + } + + Ok(()) + } + + pub(crate) async fn verify_web_tooling(&self, rustup: RustupShow) -> Result<()> { + if !rustup.has_wasm32_unknown_unknown() { + tracing::info!( + "Web platform requires wasm32-unknown-unknown to be installed. Installing..." + ); + let _ = Command::new("rustup") + .args(["target", "add", "wasm32-unknown-unknown"]) + .output() + .await?; + } + + match self.krate.wasm_bindgen_version() { + Some(version) if version == wasm_bindgen_shared::SCHEMA_VERSION => { + tracing::debug!("wasm-bindgen version {version} is compatible with dioxus-cli ✅"); + }, + Some(version) => { + tracing::warn!( + "wasm-bindgen version {version} is not compatible with the cli crate. Attempting to upgrade the target wasm-bindgen crate manually..." + ); + + let output = Command::new("cargo") + .args([ + "update", + "-p", + "wasm-bindgen", + "--precise", + &wasm_bindgen_shared::version(), + ]) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .output() + .await; + + match output { + Ok(output) if output.status.success() => tracing::info!("✅ wasm-bindgen updated successfully"), + Ok(output) => tracing::error!("Failed to update wasm-bindgen: {:?}", output), + Err(err) => tracing::error!("Failed to update wasm-bindgen: {err}"), + } + + } + None => tracing::debug!("User is attempting a web build without wasm-bindgen detected. This is probably a bug in the dioxus-cli."), + } + + Ok(()) + } + + /// Currently does nothing, but eventually we need to check that the mobile tooling is installed. + /// + /// For ios, this would be just aarch64-apple-ios + aarch64-apple-ios-sim, as well as xcrun and xcode-select + /// + /// We don't auto-install these yet since we're not doing an architecture check. We assume most users + /// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually + /// should be installing the x86 versions. + pub(crate) async fn verify_ios_tooling(&self, _rustup: RustupShow) -> Result<()> { + // open the simulator + _ = tokio::process::Command::new("open") + .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app") + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .status() + .await; + + // Now xcrun to open the device + // todo: we should try and query the device list and/or parse it rather than hardcode this simulator + _ = tokio::process::Command::new("xcrun") + .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"]) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .status() + .await; + + // if !rustup + // .installed_toolchains + // .contains(&"aarch64-apple-ios".to_string()) + // { + // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); + // } + + // if !rustup + // .installed_toolchains + // .contains(&"aarch64-apple-ios-sim".to_string()) + // { + // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); + // } + + Ok(()) + } + + /// Check if the android tooling is installed + /// + /// looks for the android sdk + ndk + /// + /// will do its best to fill in the missing bits by exploring the sdk structure + /// IE will attempt to use the Java installed from android studio if possible. + pub(crate) async fn verify_android_tooling(&self, _rustup: RustupShow) -> Result<()> { + Ok(()) + } + + /// Ensure the right dependencies are installed for linux apps. + /// This varies by distro, so we just do nothing for now. + /// + /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri: + /// https://tauri.app/start/prerequisites/ + pub(crate) async fn verify_linux_tooling(&self, _rustup: crate::RustupShow) -> Result<()> { + Ok(()) + } +} diff --git a/packages/cli/src/builder/web.rs b/packages/cli/src/builder/web.rs index b9f4ac7eb..37cb7c201 100644 --- a/packages/cli/src/builder/web.rs +++ b/packages/cli/src/builder/web.rs @@ -1,193 +1,196 @@ -use super::BuildRequest; -use super::BuildResult; -use crate::assets::pre_compress_folder; -use crate::builder::progress::Stage; -use crate::builder::progress::UpdateBuildProgress; -use crate::builder::progress::UpdateStage; -use crate::error::{Error, Result}; -use crate::TraceSrc; -use futures_channel::mpsc::UnboundedSender; -use manganis_cli_support::AssetManifest; -use std::path::Path; -use tokio::process::Command; -use wasm_bindgen_cli_support::Bindgen; +use crate::error::Result; +use crate::BuildRequest; +use std::fmt::Write; +use std::path::{Path, PathBuf}; -// Attempt to automatically recover from a bindgen failure by updating the wasm-bindgen version -async fn update_wasm_bindgen_version() -> Result<()> { - let cli_bindgen_version = wasm_bindgen_shared::version(); - tracing::info!(dx_src = ?TraceSrc::Build, "Attempting to recover from bindgen failure by setting the wasm-bindgen version to {cli_bindgen_version}..."); - - let output = Command::new("cargo") - .args([ - "update", - "-p", - "wasm-bindgen", - "--precise", - &cli_bindgen_version, - ]) - .output() - .await; - let mut error_message = None; - if let Ok(output) = output { - if output.status.success() { - tracing::info!(dx_src = ?TraceSrc::Dev, "Successfully updated wasm-bindgen to {cli_bindgen_version}"); - return Ok(()); - } else { - error_message = Some(output); - } - } - - if let Some(output) = error_message { - tracing::error!(dx_src = ?TraceSrc::Dev, "Failed to update wasm-bindgen: {:#?}", output); - } - - Err(Error::BuildFailed(format!("WASM bindgen build failed!\nThis is probably due to the Bindgen version, dioxus-cli is using `{cli_bindgen_version}` which is not compatible with your crate.\nPlease reinstall the dioxus cli to fix this issue.\nYou can reinstall the dioxus cli by running `cargo install dioxus-cli --force` and then rebuild your project"))) -} - -/// Check if the wasm32-unknown-unknown target is installed and try to install it if not -pub(crate) async fn install_web_build_tooling( - progress: &mut UnboundedSender, -) -> Result<()> { - // If the user has rustup, we can check if the wasm32-unknown-unknown target is installed - // Otherwise we can just assume it is installed - which is not great... - // Eventually we can poke at the errors and let the user know they need to install the target - if let Ok(wasm_check_command) = Command::new("rustup").args(["show"]).output().await { - let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap(); - if !wasm_check_output.contains("wasm32-unknown-unknown") { - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::InstallingWasmTooling, - update: UpdateStage::Start, - }); - tracing::info!(dx_src = ?TraceSrc::Build, "`wasm32-unknown-unknown` target not detected, installing.."); - let _ = Command::new("rustup") - .args(["target", "add", "wasm32-unknown-unknown"]) - .output() - .await?; - } - } - - Ok(()) -} +const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html"); +const TOAST_HTML: &str = include_str!("../../assets/web/toast.html"); impl BuildRequest { - async fn run_wasm_bindgen(&self, input_path: &Path, bindgen_outdir: &Path) -> Result<()> { - tracing::info!(dx_src = ?TraceSrc::Build, "Running wasm-bindgen"); - let input_path = input_path.to_path_buf(); - let bindgen_outdir = bindgen_outdir.to_path_buf(); - let keep_debug = - self.dioxus_crate.dioxus_config.web.wasm_opt.debug || (!self.build_arguments.release); - let name = self.dioxus_crate.dioxus_config.application.name.clone(); - let run_wasm_bindgen = move || { - // [3] Bindgen the final binary for use easy linking - let mut bindgen_builder = Bindgen::new(); - - bindgen_builder - .input_path(&input_path) - .web(true) - .unwrap() - .debug(keep_debug) - .demangle(keep_debug) - .keep_debug(keep_debug) - .reference_types(true) - .remove_name_section(!keep_debug) - .remove_producers_section(!keep_debug) - .out_name(&name) - .generate(&bindgen_outdir) - .unwrap(); + pub(crate) fn prepare_html(&self) -> Result { + let mut html = { + let crate_root: &Path = &self.krate.crate_dir(); + let custom_html_file = crate_root.join("index.html"); + std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML)) }; - let bindgen_result = tokio::task::spawn_blocking(run_wasm_bindgen.clone()).await; - // WASM bindgen requires the exact version of the bindgen schema to match the version the CLI was built with - // If we get an error, we can try to recover by pinning the user's wasm-bindgen version to the version we used - if let Err(err) = bindgen_result { - tracing::error!(dx_src = ?TraceSrc::Build, "Bindgen build failed: {:?}", err); - update_wasm_bindgen_version().await?; - run_wasm_bindgen(); + // Inject any resources from the config into the html + self.inject_resources(&mut html)?; + + // Inject loading scripts if they are not already present + self.inject_loading_scripts(&mut html); + + // Replace any special placeholders in the HTML with resolved values + self.replace_template_placeholders(&mut html); + + let title = self.krate.config.web.app.title.clone(); + + replace_or_insert_before("{app_title}", " bool { + !self.build.release + } + + // Inject any resources from the config into the html + fn inject_resources(&self, html: &mut String) -> Result<()> { + // Collect all resources into a list of styles and scripts + let resources = &self.krate.config.web.resource; + let mut style_list = resources.style.clone().unwrap_or_default(); + let mut script_list = resources.script.clone().unwrap_or_default(); + + if self.is_dev_build() { + style_list.extend(resources.dev.style.iter().cloned()); + script_list.extend(resources.dev.script.iter().cloned()); } + let mut head_resources = String::new(); + + // Add all styles to the head + for style in &style_list { + writeln!( + &mut head_resources, + "", + &style.to_str().unwrap(), + )?; + } + + // Add all scripts to the head + for script in &script_list { + writeln!( + &mut head_resources, + "", + &script.to_str().unwrap(), + )?; + } + + if !style_list.is_empty() { + self.send_resource_deprecation_warning(style_list, ResourceType::Style); + } + if !script_list.is_empty() { + self.send_resource_deprecation_warning(script_list, ResourceType::Script); + } + + // Inject any resources from manganis into the head + // if let Some(assets) = assets { + // head_resources.push_str(&assets.head()); + // } + + replace_or_insert_before("{style_include}", ", - progress: &mut UnboundedSender, - ) -> Result<()> { - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::OptimizingWasm, - update: UpdateStage::Start, - }); - - // Find the wasm file - let output_location = build_result.executable.clone(); - let input_path = output_location.with_extension("wasm"); - - // Create the directory where the bindgen output will be placed - let bindgen_outdir = self.target_out_dir().join("assets").join("dioxus"); - - // Run wasm-bindgen - self.run_wasm_bindgen(&input_path, &bindgen_outdir).await?; - - // Only run wasm-opt if the feature is enabled - // Wasm-opt has an expensive build script that makes it annoying to keep enabled for iterative dev - #[cfg(feature = "optimizations")] - { - // Run wasm-opt if this is a release build - if self.build_arguments.release { - use crate::config::WasmOptLevel; - - tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt..."); - let mut options = match self.dioxus_crate.dioxus_config.web.wasm_opt.level { - WasmOptLevel::Z => { - wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively() - } - WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(), - WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(), - WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(), - WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(), - WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(), - WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(), - }; - let wasm_file = bindgen_outdir.join(format!( - "{}_bg.wasm", - self.dioxus_crate.dioxus_config.application.name - )); - let old_size = wasm_file.metadata()?.len(); - options - // WASM bindgen relies on reference types - .enable_feature(wasm_opt::Feature::ReferenceTypes) - .debug_info(self.dioxus_crate.dioxus_config.web.wasm_opt.debug) - .run(&wasm_file, &wasm_file) - .map_err(|err| Error::Other(anyhow::anyhow!(err)))?; - let new_size = wasm_file.metadata()?.len(); - tracing::info!( - dx_src = ?TraceSrc::Build, - "wasm-opt reduced WASM size from {} to {} ({:2}%)", - old_size, - new_size, - (new_size as f64 - old_size as f64) / old_size as f64 * 100.0 - ); - } + /// Inject loading scripts if they are not already present + fn inject_loading_scripts(&self, html: &mut String) { + // If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything + if !self.build.inject_loading_scripts || html.contains("__wbindgen_start") { + return; } - // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output - let pre_compress = self - .dioxus_crate - .should_pre_compress_web_assets(self.build_arguments.release); - tokio::task::spawn_blocking(move || pre_compress_folder(&bindgen_outdir, pre_compress)) - .await - .unwrap()?; + // If not, insert the script + *html = html.replace( + " + // We can't use a module script here because we need to start the script immediately when streaming + import("/{base_path}/wasm/{app_name}.js").then( + ({ default: init }) => { + init("/{base_path}/wasm/{app_name}_bg.wasm").then((wasm) => { + if (wasm.__wbindgen_start == undefined) { + wasm.main(); + } + }); + } + ); + + {DX_TOAST_UTILITIES} + html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML), + false => html.replace("{DX_TOAST_UTILITIES}", ""), + }; - Ok(()) + // And try to insert preload links for the wasm and js files + *html = html.replace( + " + + , variant: ResourceType) { + const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release. Instead of including assets in the config, you can include assets with the `asset!` macro and add them to the head with `document::Link` and `Script` components."#; + + let replacement_components = paths + .iter() + .map(|path| { + let path = if path.exists() { + path.to_path_buf() + } else { + // If the path is absolute, make it relative to the current directory before we join it + // The path is actually a web path which is relative to the root of the website + let path = path.strip_prefix("/").unwrap_or(path); + let asset_dir_path = self.krate.legacy_asset_dir().join(path); + if let Ok(absolute_path) = asset_dir_path.canonicalize() { + let absolute_crate_root = self.krate.crate_dir().canonicalize().unwrap(); + PathBuf::from("./") + .join(absolute_path.strip_prefix(absolute_crate_root).unwrap()) + } else { + path.to_path_buf() + } + }; + match variant { + ResourceType::Style => { + format!(" Stylesheet {{ href: asset!(\"{}\") }}", path.display()) + } + ResourceType::Script => { + format!(" Script {{ src: asset!(\"{}\") }}", path.display()) + } + } + }) + .collect::>(); + let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n")); + let section_name = match variant { + ResourceType::Style => "web.resource.style", + ResourceType::Script => "web.resource.script", + }; + + tracing::debug!( + "{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```" + ); + } +} + +enum ResourceType { + Style, + Script, +} + +/// Replace a string or insert the new contents before a marker +fn replace_or_insert_before( + replace: &str, + or_insert_before: &str, + with: &str, + content: &mut String, +) { + if content.contains(replace) { + *content = content.replace(replace, with); + } else if let Some(pos) = content.find(or_insert_before) { + content.insert_str(pos, with); } } diff --git a/packages/cli/src/bundle_utils.rs b/packages/cli/src/bundle_utils.rs new file mode 100644 index 000000000..aaa6834c9 --- /dev/null +++ b/packages/cli/src/bundle_utils.rs @@ -0,0 +1,196 @@ +use crate::{ + config::BundleConfig, CustomSignCommandSettings, DebianSettings, MacOsSettings, + NSISInstallerMode, NsisSettings, PackageType, WebviewInstallMode, WindowsSettings, WixSettings, +}; + +pub(crate) fn make_tauri_bundler_settings( + bundle_config: BundleConfig, +) -> tauri_bundler::BundleSettings { + bundle_config.into() +} + +impl From for tauri_bundler::NsisSettings { + fn from(val: NsisSettings) -> Self { + tauri_bundler::NsisSettings { + header_image: val.header_image, + sidebar_image: val.sidebar_image, + installer_icon: val.installer_icon, + install_mode: val.install_mode.into(), + languages: val.languages, + display_language_selector: val.display_language_selector, + custom_language_files: None, + template: None, + compression: tauri_utils::config::NsisCompression::None, + start_menu_folder: val.start_menu_folder, + installer_hooks: val.installer_hooks, + minimum_webview2_version: val.minimum_webview2_version, + } + } +} + +impl From for tauri_bundler::BundleSettings { + fn from(val: BundleConfig) -> Self { + tauri_bundler::BundleSettings { + identifier: val.identifier, + publisher: val.publisher, + icon: val.icon, + resources: val.resources, + copyright: val.copyright, + category: val.category.and_then(|c| c.parse().ok()), + short_description: val.short_description, + long_description: val.long_description, + external_bin: val.external_bin, + deb: val.deb.map(Into::into).unwrap_or_default(), + macos: val.macos.map(Into::into).unwrap_or_default(), + windows: val.windows.map(Into::into).unwrap_or_default(), + ..Default::default() + } + } +} + +impl From for tauri_bundler::DebianSettings { + fn from(val: DebianSettings) -> Self { + tauri_bundler::DebianSettings { + depends: val.depends, + files: val.files, + desktop_template: val.desktop_template, + provides: val.provides, + conflicts: val.conflicts, + replaces: val.replaces, + section: val.section, + priority: val.priority, + changelog: val.changelog, + pre_install_script: val.pre_install_script, + post_install_script: val.post_install_script, + pre_remove_script: val.pre_remove_script, + post_remove_script: val.post_remove_script, + } + } +} + +impl From for tauri_bundler::WixSettings { + fn from(val: WixSettings) -> Self { + tauri_bundler::WixSettings { + language: tauri_bundler::bundle::WixLanguage({ + let mut languages: Vec<_> = val + .language + .iter() + .map(|l| { + ( + l.0.clone(), + tauri_bundler::bundle::WixLanguageConfig { + locale_path: l.1.clone(), + }, + ) + }) + .collect(); + if languages.is_empty() { + languages.push(("en-US".into(), Default::default())); + } + languages + }), + template: val.template, + fragment_paths: val.fragment_paths, + component_group_refs: val.component_group_refs, + component_refs: val.component_refs, + feature_group_refs: val.feature_group_refs, + feature_refs: val.feature_refs, + merge_refs: val.merge_refs, + enable_elevated_update_task: val.enable_elevated_update_task, + banner_path: val.banner_path, + dialog_image_path: val.dialog_image_path, + fips_compliant: val.fips_compliant, + version: val.version, + upgrade_code: val.upgrade_code, + } + } +} + +impl From for tauri_bundler::MacOsSettings { + fn from(val: MacOsSettings) -> Self { + tauri_bundler::MacOsSettings { + frameworks: val.frameworks, + minimum_system_version: val.minimum_system_version, + exception_domain: val.exception_domain, + signing_identity: val.signing_identity, + provider_short_name: val.provider_short_name, + entitlements: val.entitlements, + info_plist_path: val.info_plist_path, + files: val.files, + hardened_runtime: val.hardened_runtime, + } + } +} + +#[allow(deprecated)] +impl From for tauri_bundler::WindowsSettings { + fn from(val: WindowsSettings) -> Self { + tauri_bundler::WindowsSettings { + digest_algorithm: val.digest_algorithm, + certificate_thumbprint: val.certificate_thumbprint, + timestamp_url: val.timestamp_url, + tsp: val.tsp, + wix: val.wix.map(Into::into), + webview_install_mode: val.webview_install_mode.into(), + allow_downgrades: val.allow_downgrades, + nsis: val.nsis.map(Into::into), + sign_command: val.sign_command.map(Into::into), + + icon_path: val.icon_path.unwrap_or("./icons/icon.ico".into()), + } + } +} + +impl From for tauri_utils::config::NSISInstallerMode { + fn from(val: NSISInstallerMode) -> Self { + match val { + NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, + NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, + NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, + } + } +} + +impl From for tauri_bundler::PackageType { + fn from(value: PackageType) -> Self { + match value { + PackageType::MacOsBundle => Self::MacOsBundle, + PackageType::IosBundle => Self::IosBundle, + PackageType::WindowsMsi => Self::WindowsMsi, + PackageType::Deb => Self::Deb, + PackageType::Rpm => Self::Rpm, + PackageType::AppImage => Self::AppImage, + PackageType::Dmg => Self::Dmg, + PackageType::Updater => Self::Updater, + } + } +} + +impl WebviewInstallMode { + fn into(self) -> tauri_utils::config::WebviewInstallMode { + match self { + Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, + Self::DownloadBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } + } + Self::EmbedBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } + } + Self::OfflineInstaller { silent } => { + tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } + } + Self::FixedRuntime { path } => { + tauri_utils::config::WebviewInstallMode::FixedRuntime { path } + } + } + } +} + +impl From for tauri_bundler::CustomSignCommandSettings { + fn from(val: CustomSignCommandSettings) -> Self { + tauri_bundler::CustomSignCommandSettings { + cmd: val.cmd, + args: val.args, + } + } +} diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 3d2f5c682..6d35d5c91 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -1,6 +1,5 @@ use super::*; use crate::DioxusCrate; -use build::TargetArgs; use dioxus_autofmt::{IndentOptions, IndentType}; use rayon::prelude::*; use std::{borrow::Cow, fs, path::Path, process::exit}; @@ -10,35 +9,35 @@ use std::{borrow::Cow, fs, path::Path, process::exit}; /// Format some rsx #[derive(Clone, Debug, Parser)] -pub struct Autoformat { +pub(crate) struct Autoformat { /// Format rust code before the formatting the rsx macros #[clap(long)] - pub all_code: bool, + pub(crate) all_code: bool, /// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits /// with 1 and prints a diff if formatting is required. #[clap(short, long)] - pub check: bool, + pub(crate) check: bool, /// Input rsx (selection) #[clap(short, long)] - pub raw: Option, + pub(crate) raw: Option, /// Input file #[clap(short, long)] - pub file: Option, + pub(crate) file: Option, /// Split attributes in lines or not #[clap(short, long, default_value = "false")] - pub split_line_attributes: bool, + pub(crate) split_line_attributes: bool, /// The package to build #[clap(short, long)] - pub package: Option, + pub(crate) package: Option, } impl Autoformat { - pub fn autoformat(self) -> Result<()> { + pub(crate) fn autoformat(self) -> Result<()> { let Autoformat { check, raw, @@ -158,7 +157,7 @@ fn format_file( let mut if_write = false; if format_rust_code { let formatted = format_rust(&contents) - .map_err(|err| Error::ParseError(format!("Syntax Error:\n{}", err)))?; + .map_err(|err| Error::Parse(format!("Syntax Error:\n{}", err)))?; if contents != formatted { if_write = true; contents = formatted; @@ -166,9 +165,9 @@ fn format_file( } let parsed = syn::parse_file(&contents) - .map_err(|err| Error::ParseError(format!("Failed to parse file: {}", err)))?; + .map_err(|err| Error::Parse(format!("Failed to parse file: {}", err)))?; let edits = dioxus_autofmt::try_fmt_file(&contents, &parsed, indent) - .map_err(|err| Error::ParseError(format!("Failed to format file: {}", err)))?; + .map_err(|err| Error::Parse(format!("Failed to format file: {}", err)))?; let len = edits.len(); if !edits.is_empty() { @@ -240,8 +239,11 @@ fn indentation_for( .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .output()?; + if !out.status.success() { - return Err(Error::CargoError("cargo fmt failed".into())); + return Err(Error::Runtime(format!( + "cargo fmt failed with status: {out:?}" + ))); } let config = String::from_utf8_lossy(&out.stdout); @@ -252,18 +254,16 @@ fn indentation_for( .and_then(|line| line.split_once('=')) .map(|(_, value)| value.trim() == "true") .ok_or_else(|| { - Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into()) + Error::Runtime("Could not find hard_tabs option in rustfmt config".into()) })?; let tab_spaces = config .lines() .find(|line| line.starts_with("tab_spaces ")) .and_then(|line| line.split_once('=')) .map(|(_, value)| value.trim().parse::()) - .ok_or_else(|| { - Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into()) - })? + .ok_or_else(|| Error::Runtime("Could not find tab_spaces option in rustfmt config".into()))? .map_err(|_| { - Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into()) + Error::Runtime("Could not parse tab_spaces option in rustfmt config".into()) })?; Ok(IndentOptions::new( @@ -288,7 +288,7 @@ fn format_syn_error(err: syn::Error) -> Error { let start = err.span().start(); let line = start.line; let column = start.column; - Error::ParseError(format!( + Error::Parse(format!( "Syntax Error in line {} column {}:\n{}", line, column, err )) diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 7af213df3..d5055e4ee 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,224 +1,175 @@ -use std::str::FromStr; - -use crate::{builder::OpenArguments, config::Platform}; -use anyhow::Context; - -use crate::{ - builder::{BuildRequest, TargetPlatform}, - dioxus_crate::DioxusCrate, -}; - use super::*; - -/// Information about the target to build -#[derive(Clone, Debug, Default, Deserialize, Parser)] -pub struct TargetArgs { - /// Build for nightly [default: false] - #[clap(long)] - pub nightly: bool, - - /// Build a example [default: ""] - #[clap(long)] - pub example: Option, - - /// Build a binary [default: ""] - #[clap(long)] - pub bin: Option, - - /// The package to build - #[clap(short, long)] - pub package: Option, - - /// Space separated list of features to activate - #[clap(long)] - pub features: Vec, - - /// The feature to use for the client in a fullstack app [default: "web"] - #[clap(long)] - pub client_feature: Option, - - /// The feature to use for the server in a fullstack app [default: "server"] - #[clap(long)] - pub server_feature: Option, - - /// Rustc platform triple - #[clap(long)] - pub target: Option, -} +use crate::{AppBundle, Builder, DioxusCrate, Platform, PROFILE_SERVER}; /// Build the Rust Dioxus app and all of its assets. +/// +/// Produces a final output bundle designed to be run on the target platform. #[derive(Clone, Debug, Default, Deserialize, Parser)] #[clap(name = "build")] -pub struct Build { +pub(crate) struct BuildArgs { /// Build in release mode [default: false] #[clap(long, short)] #[serde(default)] - pub release: bool, - - /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] - #[clap(long)] - #[serde(default)] - pub force_debug: bool, + pub(crate) release: bool, /// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false] #[clap(long)] #[serde(default)] - pub force_sequential: bool, + pub(crate) force_sequential: bool, - // Use verbose output [default: false] + /// Use verbose output [default: false] #[clap(long)] #[serde(default)] - pub verbose: bool, + pub(crate) verbose: bool, - /// Build with custom profile + /// Use trace output [default: false] #[clap(long)] - pub profile: Option, + #[serde(default)] + pub(crate) trace: bool, + + /// Pass -Awarnings to the cargo build + #[clap(long)] + #[serde(default)] + pub(crate) silent: bool, + + /// Build the app with custom a profile + #[clap(long)] + pub(crate) profile: Option, + + /// Build with custom profile for the fullstack server + #[clap(long, default_value_t = PROFILE_SERVER.to_string())] + pub(crate) server_profile: String, /// Build platform: support Web & Desktop [default: "default_platform"] #[clap(long, value_enum)] - pub platform: Option, + pub(crate) platform: Option, + + /// Build the fullstack variant of this app, using that as the fileserver and backend + /// + /// This defaults to `false` but will be overridden to true if the `fullstack` feature is enabled. + #[clap(long)] + pub(crate) fullstack: bool, + + /// Run the ssg config of the app and generate the files + #[clap(long)] + pub(crate) ssg: bool, /// Skip collecting assets from dependencies [default: false] #[clap(long)] #[serde(default)] - pub skip_assets: bool, + pub(crate) skip_assets: bool, /// Extra arguments passed to cargo build #[clap(last = true)] - pub cargo_args: Vec, + pub(crate) cargo_args: Vec, /// Inject scripts to load the wasm and js files for your dioxus app if they are not already present [default: true] #[clap(long, default_value_t = true)] - pub inject_loading_scripts: bool, + pub(crate) inject_loading_scripts: bool, /// Information about the target to build #[clap(flatten)] - pub target_args: TargetArgs, + pub(crate) target_args: TargetArgs, } -impl Build { - pub fn resolve(&mut self, dioxus_crate: &mut DioxusCrate) -> Result<()> { - // Inherit the platform from the defaults +impl BuildArgs { + pub async fn build_it(&mut self) -> Result<()> { + self.build().await?; + Ok(()) + } + + pub(crate) async fn build(&mut self) -> Result { + let krate = + DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?; + + self.resolve(&krate)?; + + let bundle = Builder::start(&krate, self.clone())?.finish().await?; + + println!( + "Successfully built! 💫\nBundle at {}", + bundle.app_dir().display() + ); + + Ok(bundle) + } + + /// Update the arguments of the CLI by inspecting the DioxusCrate itself and learning about how + /// the user has configured their app. + /// + /// IE if they've specified "fullstack" as a feature on `dioxus`, then we want to build the + /// fullstack variant even if they omitted the `--fullstack` flag. + pub(crate) fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> { + let default_platform = krate.default_platform(); + let auto_platform = krate.autodetect_platform(); + + // The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml + // We want to strip out the default platform and use the one they passed, setting no-default-features + if self.platform.is_some() && default_platform.is_some() { + self.target_args.no_default_features = true; + self.target_args + .features + .extend(krate.platformless_features()); + } + + // Inherit the platform from the args, or auto-detect it + if self.platform.is_none() { + let (platform, _feature) = auto_platform.ok_or_else(|| { + anyhow::anyhow!("No platform was specified and could not be auto-detected. Please specify a platform with `--platform ` or set a default platform using a cargo feature.") + })?; + self.platform = Some(platform); + } + let platform = self .platform - .unwrap_or_else(|| self.auto_detect_platform(dioxus_crate)); - self.platform = Some(platform); + .expect("Platform to be set after autodetection"); - // Add any features required to turn on the platform we are building for + // Add any features required to turn on the client self.target_args - .features - .extend(dioxus_crate.features_for_platform(platform)); + .client_features + .extend(krate.feature_for_platform(platform)); - Ok(()) - } + // Add any features required to turn on the server + // This won't take effect in the server is not built, so it's fine to just set it here even if it's not used + self.target_args + .server_features + .extend(krate.feature_for_platform(Platform::Server)); - pub async fn build(&mut self, dioxus_crate: &mut DioxusCrate) -> Result<()> { - self.resolve(dioxus_crate)?; - let build_requests = BuildRequest::create(false, dioxus_crate, self.clone())?; - let builds = BuildRequest::build_all_parallel(build_requests).await?; + // Make sure we set the fullstack platform so we actually build the fullstack variant + // Users need to enable "fullstack" in their default feature set. + // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled + self.fullstack = self.fullstack || krate.has_dioxus_feature("fullstack"); - // If this is a static generation build, building involves running the server to generate static files - if self.platform.unwrap() == Platform::StaticGeneration { - println!("Building static site..."); - for build in builds { - if let Some(mut result) = - build.open(OpenArguments::new_for_static_generation_build(dioxus_crate))? - { - result.wait().await?; + // Make sure we have a server feature if we're building a fullstack app + // + // todo(jon): eventually we want to let users pass a `--server ` flag to specify a package to use as the server + // however, it'll take some time to support that and we don't have a great RPC binding layer between the two yet + if self.fullstack && self.target_args.server_features.is_empty() { + return Err(anyhow::anyhow!("Fullstack builds require a server feature on the target crate. Add a `server` feature to the crate and try again.").into()); + } + + // Set the profile of the build if it's not already set + // We do this for android/wasm since they require + if self.profile.is_none() { + match self.platform { + Some(Platform::Android) => { + self.profile = Some(crate::dioxus_crate::PROFILE_ANDROID.to_string()); } - } - println!("Static site built!"); - } - Ok(()) - } - - pub async fn run(&mut self) -> anyhow::Result<()> { - let mut dioxus_crate = - DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?; - self.build(&mut dioxus_crate).await?; - Ok(()) - } - - pub(crate) fn auto_detect_client_platform( - &self, - resolved: &DioxusCrate, - ) -> (Option, TargetPlatform) { - self.find_dioxus_feature(resolved, |platform| { - matches!(platform, TargetPlatform::Web | TargetPlatform::Desktop) - }) - .unwrap_or_else(|| (Some("web".to_string()), TargetPlatform::Web)) - } - - pub(crate) fn auto_detect_server_feature(&self, resolved: &DioxusCrate) -> Option { - self.find_dioxus_feature(resolved, |platform| { - matches!(platform, TargetPlatform::Server) - }) - .map(|(feature, _)| feature) - .unwrap_or_else(|| Some("server".to_string())) - } - - fn auto_detect_platform(&self, resolved: &DioxusCrate) -> Platform { - self.auto_detect_platform_with_filter(resolved, |_| true).1 - } - - fn auto_detect_platform_with_filter( - &self, - resolved: &DioxusCrate, - filter_platform: fn(&Platform) -> bool, - ) -> (Option, Platform) { - self.find_dioxus_feature(resolved, filter_platform) - .unwrap_or_else(|| { - let default_platform = resolved.dioxus_config.application.default_platform; - - (Some(default_platform.to_string()), default_platform) - }) - } - - fn find_dioxus_feature( - &self, - resolved: &DioxusCrate, - filter_platform: fn(&P) -> bool, - ) -> Option<(Option, P)> { - // First check the enabled features for any renderer enabled - for dioxus in resolved.krates.krates_by_name("dioxus") { - let Some(features) = resolved.krates.get_enabled_features(dioxus.kid) else { - continue; - }; - - if let Some(platform) = features - .iter() - .find_map(|platform| platform.parse::

().ok()) - .filter(filter_platform) - { - return Some((None, platform)); + Some(Platform::Web) => { + self.profile = Some(crate::dioxus_crate::PROFILE_WASM.to_string()); + } + Some(Platform::Server) => { + self.profile = Some(crate::dioxus_crate::PROFILE_SERVER.to_string()); + } + _ => {} } } - // Then check the features that might get enabled - if let Some(platform) = resolved - .package() - .features - .iter() - .find_map(|(feature, enables)| { - enables - .iter() - .find_map(|f| { - f.strip_prefix("dioxus/") - .or_else(|| feature.strip_prefix("dep:dioxus/")) - .and_then(|f| f.parse::

().ok()) - .filter(filter_platform) - }) - .map(|platform| (Some(feature.clone()), platform)) - }) - { - return Some(platform); - } - - None + Ok(()) } /// Get the platform from the build arguments - pub fn platform(&self) -> Platform { - self.platform.unwrap_or_default() + pub(crate) fn platform(&self) -> Platform { + self.platform.expect("Platform was not set") } } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index c0470290d..e53c2929a 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,11 +1,10 @@ -use crate::build::Build; +use crate::bundle_utils::make_tauri_bundler_settings; use crate::DioxusCrate; +use crate::{build::BuildArgs, PackageType}; use anyhow::Context; use std::env::current_dir; -use std::fs::create_dir_all; -use std::ops::Deref; use std::str::FromStr; -use tauri_bundler::{BundleSettings, PackageSettings, SettingsBuilder}; +use tauri_bundler::{PackageSettings, SettingsBuilder}; use super::*; @@ -16,77 +15,26 @@ pub struct Bundle { /// The package types to bundle #[clap(long)] pub packages: Option>, + /// The arguments for the dioxus build #[clap(flatten)] - pub build_arguments: Build, -} - -impl Deref for Bundle { - type Target = Build; - - fn deref(&self) -> &Self::Target { - &self.build_arguments - } -} - -#[derive(Clone, Copy, Debug)] -pub enum PackageType { - MacOsBundle, - IosBundle, - WindowsMsi, - Deb, - Rpm, - AppImage, - Dmg, - Updater, -} - -impl FromStr for PackageType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "macos" => Ok(PackageType::MacOsBundle), - "ios" => Ok(PackageType::IosBundle), - "msi" => Ok(PackageType::WindowsMsi), - "deb" => Ok(PackageType::Deb), - "rpm" => Ok(PackageType::Rpm), - "appimage" => Ok(PackageType::AppImage), - "dmg" => Ok(PackageType::Dmg), - _ => Err(format!("{} is not a valid package type", s)), - } - } -} - -impl From for tauri_bundler::PackageType { - fn from(val: PackageType) -> Self { - match val { - PackageType::MacOsBundle => tauri_bundler::PackageType::MacOsBundle, - PackageType::IosBundle => tauri_bundler::PackageType::IosBundle, - PackageType::WindowsMsi => tauri_bundler::PackageType::WindowsMsi, - PackageType::Deb => tauri_bundler::PackageType::Deb, - PackageType::Rpm => tauri_bundler::PackageType::Rpm, - PackageType::AppImage => tauri_bundler::PackageType::AppImage, - PackageType::Dmg => tauri_bundler::PackageType::Dmg, - PackageType::Updater => tauri_bundler::PackageType::Updater, - } - } + pub(crate) build_arguments: BuildArgs, } impl Bundle { - pub async fn bundle(mut self) -> anyhow::Result<()> { - let mut dioxus_crate = DioxusCrate::new(&self.build_arguments.target_args) + pub(crate) async fn bundle(mut self) -> anyhow::Result<()> { + let krate = DioxusCrate::new(&self.build_arguments.target_args) .context("Failed to load Dioxus workspace")?; - self.build_arguments.resolve(&mut dioxus_crate)?; + self.build_arguments.resolve(&krate)?; // Build the app - self.build_arguments.build(&mut dioxus_crate).await?; + let bundle = self.build_arguments.build().await?; // copy the binary to the out dir - let package = dioxus_crate.package(); + let package = krate.package(); - let mut name: PathBuf = dioxus_crate.executable_name().into(); + let mut name: PathBuf = krate.executable_name().into(); if cfg!(windows) { name.set_extension("exe"); } @@ -94,56 +42,32 @@ impl Bundle { // bundle the app let binaries = vec![ tauri_bundler::BundleBinary::new(name.display().to_string(), true) - .set_src_path(Some(dioxus_crate.workspace_dir().display().to_string())), + .set_src_path(Some(krate.workspace_dir().display().to_string())), ]; - let mut bundle_settings: BundleSettings = dioxus_crate.dioxus_config.bundle.clone().into(); + let bundle_config = krate.config.bundle.clone(); + let mut bundle_settings = make_tauri_bundler_settings(bundle_config); + if cfg!(windows) { - let windows_icon_override = dioxus_crate - .dioxus_config - .bundle - .windows - .as_ref() - .map(|w| &w.icon_path); + let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path); if windows_icon_override.is_none() { let icon_path = bundle_settings .icon .as_ref() .and_then(|icons| icons.first()); - let icon_path = if let Some(icon_path) = icon_path { - icon_path.into() - } else { - let path = PathBuf::from("./icons/icon.ico"); - // create the icon if it doesn't exist - if !path.exists() { - create_dir_all(path.parent().unwrap()).unwrap(); - let mut file = File::create(&path).unwrap(); - file.write_all(include_bytes!("../../assets/icon.ico")) - .unwrap(); - } - path + + if let Some(icon_path) = icon_path { + bundle_settings.icon = Some(vec![icon_path.into()]); }; - bundle_settings.windows.icon_path = icon_path; } } - // Copy the assets in the dist directory to the bundle - let static_asset_output_dir = &dioxus_crate.dioxus_config.application.out_dir; - // Make sure the dist directory is relative to the crate directory - let static_asset_output_dir = static_asset_output_dir - .strip_prefix(dioxus_crate.workspace_dir()) - .unwrap_or(static_asset_output_dir); - - let static_asset_output_dir = static_asset_output_dir.display().to_string(); - println!("Adding assets from {} to bundle", static_asset_output_dir); - // Don't copy the executable or the old bundle directory - let ignored_files = [ - dioxus_crate.out_dir().join("bundle"), - dioxus_crate.out_dir().join(name), - ]; + let ignored_files = [krate + .bundle_dir(self.build_arguments.platform()) + .join("bundle")]; - for entry in std::fs::read_dir(&static_asset_output_dir)?.flatten() { + for entry in std::fs::read_dir(bundle.asset_dir())?.flatten() { let path = entry.path().canonicalize()?; if ignored_files.iter().any(|f| path.starts_with(f)) { continue; @@ -173,14 +97,14 @@ impl Bundle { } let mut settings = SettingsBuilder::new() - .project_out_directory(dioxus_crate.out_dir()) + .project_out_directory(krate.bundle_dir(self.build_arguments.platform())) .package_settings(PackageSettings { - product_name: dioxus_crate.dioxus_config.application.name.clone(), + product_name: krate.executable_name().to_string(), version: package.version.to_string(), description: package.description.clone().unwrap_or_default(), homepage: Some(package.homepage.clone().unwrap_or_default()), authors: Some(package.authors.clone()), - default_run: Some(dioxus_crate.dioxus_config.application.name.clone()), + default_run: Some(krate.executable_name().to_string()), }) .binaries(binaries) .bundle_settings(bundle_settings); @@ -188,7 +112,7 @@ impl Bundle { settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect()); } - if let Some(target) = &self.target_args.target { + if let Some(target) = self.build_arguments.target_args.target.as_ref() { settings = settings.target(target.to_string()); } @@ -198,7 +122,7 @@ impl Bundle { #[cfg(target_os = "macos")] std::env::set_var("CI", "true"); - tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{ + tauri_bundler::bundle::bundle_project(&settings.unwrap()).unwrap_or_else(|err|{ #[cfg(target_os = "macos")] panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); #[cfg(not(target_os = "macos"))] @@ -208,3 +132,21 @@ impl Bundle { Ok(()) } } + +impl FromStr for PackageType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "macos" => Ok(PackageType::MacOsBundle), + "ios" => Ok(PackageType::IosBundle), + "msi" => Ok(PackageType::WindowsMsi), + "deb" => Ok(PackageType::Deb), + "rpm" => Ok(PackageType::Rpm), + "appimage" => Ok(PackageType::AppImage), + "dmg" => Ok(PackageType::Dmg), + "updater" => Ok(PackageType::Updater), + _ => Err(format!("{} is not a valid package type", s)), + } + } +} diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index 84374184d..f32b8c38b 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -1,29 +1,26 @@ -use crate::build::TargetArgs; +use super::*; +use crate::DioxusCrate; use futures_util::{stream::FuturesUnordered, StreamExt}; use std::{path::Path, process::exit}; -use crate::DioxusCrate; - -use super::*; - // For reference, the rustfmt main.rs file // https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs /// Check the Rust files in the project for issues. #[derive(Clone, Debug, Parser)] -pub struct Check { +pub(crate) struct Check { /// Input file #[clap(short, long)] - pub file: Option, + pub(crate) file: Option, /// Information about the target to check #[clap(flatten)] - pub target_args: TargetArgs, + pub(crate) target_args: TargetArgs, } impl Check { // Todo: check the entire crate - pub async fn check(self) -> Result<()> { + pub(crate) async fn check(self) -> Result<()> { match self.file { // Default to checking the project None => { diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 2ea627b6c..cb69935e4 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -1,19 +1,15 @@ -use crate::DioxusCrate; -use anyhow::Context; -use build::TargetArgs; - use super::*; /// Clean build artifacts. +/// +/// Simlpy runs `cargo clean` #[derive(Clone, Debug, Parser)] #[clap(name = "clean")] -pub struct Clean {} +pub(crate) struct Clean {} impl Clean { - pub fn clean(self) -> anyhow::Result<()> { - let dioxus_crate = - DioxusCrate::new(&TargetArgs::default()).context("Failed to load Dioxus workspace")?; - + /// todo(jon): we should add a config option that just wipes target/dx and target/dioxus-client instead of doing a full clean + pub(crate) fn clean(self) -> anyhow::Result<()> { let output = Command::new("cargo") .arg("clean") .stdout(Stdio::piped()) @@ -24,11 +20,6 @@ impl Clean { return Err(anyhow::anyhow!("Cargo clean failed.")); } - let out_dir = &dioxus_crate.out_dir(); - if out_dir.is_dir() { - remove_dir_all(out_dir)?; - } - Ok(()) } } diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index a1d9916e6..a6b7d31d4 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -1,13 +1,11 @@ -use crate::build::TargetArgs; +use super::*; use crate::TraceSrc; use crate::{metadata::crate_root, CliSettings}; -use super::*; - /// Dioxus config file controls #[derive(Clone, Debug, Deserialize, Subcommand)] #[clap(name = "config")] -pub enum Config { +pub(crate) enum Config { /// Init `Dioxus.toml` for project/folder. Init { /// Init project name @@ -22,8 +20,10 @@ pub enum Config { #[clap(long, default_value = "web")] platform: String, }, + /// Format print Dioxus config. FormatPrint {}, + /// Create a custom html file. CustomHtml {}, @@ -36,7 +36,7 @@ pub enum Config { } #[derive(Debug, Clone, Copy, Deserialize, Subcommand)] -pub enum Setting { +pub(crate) enum Setting { /// Set the value of the always-hot-reload setting. AlwaysHotReload { value: BoolValue }, /// Set the value of the always-open-browser setting. @@ -61,7 +61,7 @@ impl Display for Setting { // Clap complains if we use a bool directly and I can't find much info about it. // "Argument 'value` is positional and it must take a value but action is SetTrue" #[derive(Debug, Clone, Copy, Deserialize, clap::ValueEnum)] -pub enum BoolValue { +pub(crate) enum BoolValue { True, False, } @@ -76,7 +76,7 @@ impl From for bool { } impl Config { - pub fn config(self) -> Result<()> { + pub(crate) fn config(self) -> Result<()> { let crate_root = crate_root()?; match self { Config::Init { @@ -101,13 +101,13 @@ impl Config { Config::FormatPrint {} => { println!( "{:#?}", - crate::dioxus_crate::DioxusCrate::new(&TargetArgs::default())?.dioxus_config + crate::dioxus_crate::DioxusCrate::new(&TargetArgs::default())?.config ); } Config::CustomHtml {} => { let html_path = crate_root.join("index.html"); let mut file = File::create(html_path)?; - let content = include_str!("../../assets/index.html"); + let content = include_str!("../../assets/web/index.html"); file.write_all(content.as_bytes())?; tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done."); } diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index eaac21395..e0cce2af4 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -8,7 +8,7 @@ pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template"; #[derive(Clone, Debug, Default, Deserialize, Parser)] #[clap(name = "new")] -pub struct Create { +pub(crate) struct Create { /// Project name (required when `--yes` is used) name: Option, @@ -39,7 +39,7 @@ pub struct Create { } impl Create { - pub fn create(mut self) -> Result<()> { + pub(crate) fn create(mut self) -> Result<()> { let metadata = cargo_metadata::MetadataCommand::new().exec().ok(); // If we're getting pass a `.` name, that's actually a path @@ -112,7 +112,7 @@ impl Create { /// Post-creation actions for newly setup crates. // Also used by `init`. -pub fn post_create(path: &Path, metadata: Option) -> Result<()> { +pub(crate) fn post_create(path: &Path, metadata: Option) -> Result<()> { // 1. Add the new project to the workspace, if it exists. // This must be executed first in order to run `cargo fmt` on the new project. metadata.and_then(|metadata| { diff --git a/packages/cli/src/cli/doctor.rs b/packages/cli/src/cli/doctor.rs new file mode 100644 index 000000000..7c89c076d --- /dev/null +++ b/packages/cli/src/cli/doctor.rs @@ -0,0 +1,10 @@ +use clap::Parser; + +#[derive(Clone, Debug, Parser)] +pub struct Doctor {} + +impl Doctor { + pub async fn run(self) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/packages/cli/src/cli/init.rs b/packages/cli/src/cli/init.rs index 01e49b69c..f57d1eeaf 100644 --- a/packages/cli/src/cli/init.rs +++ b/packages/cli/src/cli/init.rs @@ -4,7 +4,7 @@ use cargo_generate::{GenerateArgs, TemplatePath}; #[derive(Clone, Debug, Default, Deserialize, Parser)] #[clap(name = "init")] -pub struct Init { +pub(crate) struct Init { /// Template path #[clap(default_value = DEFAULT_TEMPLATE, short, long)] template: String, @@ -24,19 +24,21 @@ pub struct Init { } impl Init { - pub fn init(self) -> Result<()> { + pub(crate) fn init(self) -> Result<()> { let metadata = cargo_metadata::MetadataCommand::new().exec().ok(); // Get directory name. let name = std::env::current_dir()? .file_name() .map(|f| f.to_str().unwrap().to_string()); + // https://github.com/console-rs/dialoguer/issues/294 ctrlc::set_handler(move || { let _ = console::Term::stdout().show_cursor(); std::process::exit(0); }) .expect("ctrlc::set_handler"); + let args = GenerateArgs { define: self.option, init: true, diff --git a/packages/cli/src/cli/link.rs b/packages/cli/src/cli/link.rs index 730c5639e..49d75504c 100644 --- a/packages/cli/src/cli/link.rs +++ b/packages/cli/src/cli/link.rs @@ -1,39 +1,108 @@ -use crate::{assets, error::Result}; -use clap::Parser; -use std::{fs, path::PathBuf}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; -#[derive(Clone, Debug, Parser)] -#[clap(name = "link", hide = true)] -pub struct LinkCommand { - // Allow us to accept any argument after `dx link` - #[clap(trailing_var_arg = true, allow_hyphen_values = true)] - pub args: Vec, +use crate::assets::AssetManifest; + +#[derive(Debug, Serialize, Deserialize)] +pub enum LinkAction { + BuildAssetManifest { + destination: PathBuf, + }, + LinkAndroid { + linker: PathBuf, + extra_flags: Vec, + }, } -impl LinkCommand { - pub fn link(self) -> Result<()> { - let Some((link_args, object_files)) = manganis_cli_support::linker_intercept(self.args) - else { - tracing::warn!("Invalid linker arguments."); - return Ok(()); - }; +impl LinkAction { + pub(crate) const ENV_VAR_NAME: &'static str = "dx-magic-link-file"; - // Parse object files, deserialize JSON, & create a file to propagate JSON. - let json = manganis_cli_support::get_json_from_object_files(object_files); - let parsed = serde_json::to_string(&json).unwrap(); + /// Should we write the input arguments to a file (aka act as a linker subprocess)? + /// + /// Just check if the magic env var is set + pub(crate) fn from_env() -> Option { + std::env::var(Self::ENV_VAR_NAME) + .ok() + .map(|var| serde_json::from_str(&var).expect("Failed to parse magic env var")) + } - let out_dir = PathBuf::from(link_args.first().unwrap()); - fs::create_dir_all(&out_dir).unwrap(); + pub(crate) fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } - let path = out_dir.join(assets::MG_JSON_OUT); - fs::write(path, parsed).unwrap(); + /// Write the incoming linker args to a file + /// + /// The file will be given by the dx-magic-link-arg env var itself, so we use + /// it both for determining if we should act as a linker and the for the file name itself. + /// + /// This will panic if it fails + /// + /// hmmmmmmmm tbh I'd rather just pass the object files back and do the parsing here, but the interface + /// is nicer to just bounce back the args and let the host do the parsing/canonicalization + pub(crate) fn run(self) -> anyhow::Result<()> { + match self { + // Literally just run the android linker :) + LinkAction::LinkAndroid { + linker, + extra_flags, + } => { + let mut cmd = std::process::Command::new(linker); + cmd.args(std::env::args().skip(1)); + cmd.args(extra_flags); + cmd.stderr(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .status() + .expect("Failed to run android linker"); + } + + // Assemble an asset manifest by walking the object files being passed to us + LinkAction::BuildAssetManifest { destination: dest } => { + let mut args: Vec<_> = std::env::args().collect(); + let mut manifest = AssetManifest::default(); + + // Handle command files, usually a windows thing. + if let Some(command) = args.iter().find(|arg| arg.starts_with('@')).cloned() { + let path = command.trim().trim_start_matches('@'); + let file_binary = std::fs::read(path).unwrap(); + + // This may be a utf-16le file. Let's try utf-8 first. + let content = String::from_utf8(file_binary.clone()).unwrap_or_else(|_| { + // Convert Vec to Vec to convert into a String + let binary_u16le: Vec = file_binary + .chunks_exact(2) + .map(|a| u16::from_le_bytes([a[0], a[1]])) + .collect(); + + String::from_utf16_lossy(&binary_u16le) + }); + + // Gather linker args, and reset the args to be just the linker args + args = content + .lines() + .map(|line| { + let line_parsed = line.to_string(); + let line_parsed = line_parsed.trim_end_matches('"').to_string(); + let line_parsed = line_parsed.trim_start_matches('"').to_string(); + line_parsed + }) + .collect(); + } + + // Parse through linker args for `.o` or `.rlib` files. + for item in args { + if item.ends_with(".o") || item.ends_with(".rlib") { + let path_to_item = PathBuf::from(item); + if let Ok(path) = path_to_item.canonicalize() { + _ = manifest.add_from_object_path(path); + } + } + } + + let contents = serde_json::to_string(&manifest).expect("Failed to write manifest"); + std::fs::write(dest, contents).expect("Failed to write output file"); + } + } Ok(()) } - - /// We need to pass the subcommand name to Manganis so this - /// helps centralize where we set the subcommand "name". - pub fn command_name() -> String { - "link".to_string() - } } diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 4ce83e12a..c901f1802 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -1,62 +1,62 @@ -pub mod autoformat; -pub mod build; -pub mod bundle; -pub mod check; -pub mod clean; -pub mod config; -pub mod create; -pub mod init; -pub mod link; -pub mod serve; -pub mod translate; +pub(crate) mod autoformat; +pub(crate) mod build; +pub(crate) mod bundle; +pub(crate) mod check; +pub(crate) mod clean; +pub(crate) mod config; +pub(crate) mod create; +pub(crate) mod doctor; +pub(crate) mod init; +pub(crate) mod link; +pub(crate) mod run; +pub(crate) mod serve; +pub(crate) mod target; +pub(crate) mod translate; -use crate::{custom_error, error::Result, Error}; +pub(crate) use build::*; +pub(crate) use serve::*; +pub(crate) use target::*; + +use crate::{error::Result, Error}; +use anyhow::Context; use clap::{Parser, Subcommand}; use html_parser::Dom; use once_cell::sync::Lazy; use serde::Deserialize; use std::{ fmt::Display, - fs::{remove_dir_all, File}, + fs::File, io::{Read, Write}, path::PathBuf, process::{Command, Stdio}, }; -pub static VERSION: Lazy = Lazy::new(|| { - format!( - "{} ({})", - crate::dx_build_info::PKG_VERSION, - crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository") - ) -}); - /// Build, Bundle & Ship Dioxus Apps. #[derive(Parser)] #[clap(name = "dioxus", version = VERSION.as_str())] -pub struct Cli { +pub(crate) struct Cli { #[clap(subcommand)] - pub action: Commands, + pub(crate) action: Commands, /// Enable verbose logging. #[clap(short)] - pub v: bool, + pub(crate) v: bool, /// Specify a binary target. #[clap(global = true, long)] - pub bin: Option, + pub(crate) bin: Option, } #[derive(Parser)] -pub enum Commands { +pub(crate) enum Commands { /// Build the Dioxus project and all of its assets. - Build(build::Build), + Build(build::BuildArgs), /// Translate a source file into Dioxus code. Translate(translate::Translate), /// Build, watch & serve the Dioxus project and all of its assets. - Serve(serve::Serve), + Serve(serve::ServeArgs), /// Create a new project for Dioxus. New(create::Create), @@ -79,13 +79,17 @@ pub enum Commands { #[clap(name = "check")] Check(check::Check), + /// Run the project without any hotreloading + #[clap(name = "run")] + Run(run::RunArgs), + + /// Ensure all the tooling is installed and configured correctly + #[clap(name = "doctor")] + Doctor(doctor::Doctor), + /// Dioxus config file controls. #[clap(subcommand)] Config(config::Config), - - /// Handles parsing of linker arguments for linker-based systems - /// such as Manganis and binary patching. - Link(link::LinkCommand), } impl Display for Commands { @@ -101,7 +105,16 @@ impl Display for Commands { Commands::Autoformat(_) => write!(f, "fmt"), Commands::Check(_) => write!(f, "check"), Commands::Bundle(_) => write!(f, "bundle"), - Commands::Link(_) => write!(f, "link"), + Commands::Run(_) => write!(f, "run"), + Commands::Doctor(_) => write!(f, "doctor"), } } } + +pub(crate) static VERSION: Lazy = Lazy::new(|| { + format!( + "{} ({})", + crate::dx_build_info::PKG_VERSION, + crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository") + ) +}); diff --git a/packages/cli/src/cli/run.rs b/packages/cli/src/cli/run.rs new file mode 100644 index 000000000..728e4bbe6 --- /dev/null +++ b/packages/cli/src/cli/run.rs @@ -0,0 +1,60 @@ +use super::*; +use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate}; + +/// Run the project with the given arguments +#[derive(Clone, Debug, Parser)] +pub(crate) struct RunArgs { + /// Information about the target to build + #[clap(flatten)] + pub(crate) build_args: BuildArgs, +} + +impl RunArgs { + pub(crate) async fn run(mut self) -> anyhow::Result<()> { + let krate = DioxusCrate::new(&self.build_args.target_args) + .context("Failed to load Dioxus workspace")?; + + self.build_args.resolve(&krate)?; + + println!("Building crate krate data: {:#?}", krate); + println!("Build args: {:#?}", self.build_args); + + let bundle = Builder::start(&krate, self.build_args.clone())? + .finish() + .await?; + + let devserver_ip = "127.0.0.1:8080".parse().unwrap(); + let fullstack_ip = "127.0.0.1:8081".parse().unwrap(); + + let mut runner = crate::serve::AppRunner::start(&krate); + runner + .open(bundle, devserver_ip, Some(fullstack_ip), true) + .await?; + + // Run the app, but mostly ignore all the other messages + // They won't generally be emitted + loop { + match runner.wait().await { + ServeUpdate::StderrReceived { platform, msg } => println!("[{platform}]: {msg}"), + ServeUpdate::StdoutReceived { platform, msg } => println!("[{platform}]: {msg}"), + ServeUpdate::ProcessExited { platform, status } => { + runner.kill(platform); + eprintln!("[{platform}]: process exited with status: {status:?}"); + break; + } + ServeUpdate::BuildUpdate { .. } => {} + ServeUpdate::TracingLog { .. } => {} + ServeUpdate::Exit { .. } => break, + ServeUpdate::NewConnection => {} + ServeUpdate::WsMessage(_) => {} + ServeUpdate::FilesChanged { .. } => {} + ServeUpdate::RequestRebuild => {} + ServeUpdate::Redraw => {} + ServeUpdate::OpenApp => {} + ServeUpdate::ToggleShouldRebuild => {} + } + } + + Ok(()) + } +} diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index eac315648..4ca815046 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -1,120 +1,108 @@ -use crate::config::AddressArguments; -use crate::{settings, tracer::CLILogControl, DioxusCrate}; -use anyhow::Context; -use build::Build; -use std::ops::Deref; - use super::*; +use crate::{AddressArguments, BuildArgs, DioxusCrate, Platform}; +use anyhow::Context; -/// Arguments for the serve command -#[derive(Clone, Debug, Parser, Default)] -pub struct ServeArguments { +/// Serve the project +#[derive(Clone, Debug, Default, Parser)] +#[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))] +#[clap(name = "serve")] +pub(crate) struct ServeArgs { /// The arguments for the address the server will run on #[clap(flatten)] - pub address: AddressArguments, + pub(crate) address: AddressArguments, /// Open the app in the default browser [default: true - unless cli settings are set] #[arg(long, default_missing_value="true", num_args=0..=1)] - pub open: Option, + pub(crate) open: Option, /// Enable full hot reloading for the app [default: true - unless cli settings are set] #[clap(long, group = "release-incompatible")] - pub hot_reload: Option, + pub(crate) hot_reload: Option, /// Configure always-on-top for desktop apps [default: true - unless cli settings are set] #[clap(long, default_missing_value = "true")] - pub always_on_top: Option, + pub(crate) always_on_top: Option, /// Set cross-origin-policy to same-origin [default: false] #[clap(name = "cross-origin-policy")] #[clap(long)] - pub cross_origin_policy: bool, + pub(crate) cross_origin_policy: bool, /// Additional arguments to pass to the executable #[clap(long)] - pub args: Vec, + pub(crate) args: Vec, /// Sets the interval in seconds that the CLI will poll for file changes on WSL. #[clap(long, default_missing_value = "2")] - pub wsl_file_poll_interval: Option, -} - -/// Run the WASM project on dev-server -#[derive(Clone, Debug, Default, Parser)] -#[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))] -#[clap(name = "serve")] -pub struct Serve { - /// Arguments for the serve command - #[clap(flatten)] - pub(crate) server_arguments: ServeArguments, - - /// Arguments for the dioxus build - #[clap(flatten)] - pub(crate) build_arguments: Build, + pub(crate) wsl_file_poll_interval: Option, /// Run the server in interactive mode #[arg(long, default_missing_value="true", num_args=0..=1, short = 'i')] - pub interactive: Option, + pub(crate) interactive: Option, + + /// Arguments for the build itself + #[clap(flatten)] + pub(crate) build_arguments: BuildArgs, } -impl Serve { - /// Resolve the serve arguments from the arguments or the config - fn resolve(&mut self, crate_config: &mut DioxusCrate) -> Result<()> { - // Set config settings. - let settings = settings::CliSettings::load(); +impl ServeArgs { + /// Start the tui, builder, etc by resolving the arguments and then running the actual top-level serve function + pub(crate) async fn serve(mut self) -> Result<()> { + let krate = DioxusCrate::new(&self.build_arguments.target_args) + .context("Failed to load Dioxus workspace")?; // Enable hot reload. - if self.server_arguments.hot_reload.is_none() { - self.server_arguments.hot_reload = Some(settings.always_hot_reload.unwrap_or(true)); + if self.hot_reload.is_none() { + self.hot_reload = Some(krate.settings.always_hot_reload.unwrap_or(true)); } // Open browser. - if self.server_arguments.open.is_none() { - self.server_arguments.open = Some(settings.always_open_browser.unwrap_or_default()); + if self.open.is_none() { + self.open = Some(krate.settings.always_open_browser.unwrap_or_default()); } // Set WSL file poll interval. - if self.server_arguments.wsl_file_poll_interval.is_none() { - self.server_arguments.wsl_file_poll_interval = - Some(settings.wsl_file_poll_interval.unwrap_or(2)); + if self.wsl_file_poll_interval.is_none() { + self.wsl_file_poll_interval = Some(krate.settings.wsl_file_poll_interval.unwrap_or(2)); } // Set always-on-top for desktop. - if self.server_arguments.always_on_top.is_none() { - self.server_arguments.always_on_top = Some(settings.always_on_top.unwrap_or(true)) + if self.always_on_top.is_none() { + self.always_on_top = Some(krate.settings.always_on_top.unwrap_or(true)) } - crate_config.dioxus_config.desktop.always_on_top = - self.server_arguments.always_on_top.unwrap_or(true); // Resolve the build arguments - self.build_arguments.resolve(crate_config)?; + self.build_arguments.resolve(&krate)?; - // Since this is a serve, adjust the outdir to be target/dx-dist/ - let mut dist_dir = crate_config.workspace_dir().join("target").join("dx-dist"); + // Give us some space before we start printing things... + println!(); - if crate_config.target.is_example() { - dist_dir = dist_dir.join("examples"); - } - - crate_config.dioxus_config.application.out_dir = - dist_dir.join(crate_config.executable_name()); - - Ok(()) + crate::serve::serve_all(self, krate).await } - pub async fn serve(mut self, log_control: CLILogControl) -> anyhow::Result<()> { - let mut dioxus_crate = DioxusCrate::new(&self.build_arguments.target_args) - .context("Failed to load Dioxus workspace")?; + pub(crate) fn should_hotreload(&self) -> bool { + self.hot_reload.unwrap_or(true) + } - self.resolve(&mut dioxus_crate)?; + pub(crate) fn build_args(&self) -> BuildArgs { + self.build_arguments.clone() + } - crate::serve::serve_all(self, dioxus_crate, log_control).await?; - Ok(()) + pub(crate) fn is_interactive_tty(&self) -> bool { + use crossterm::tty::IsTty; + std::io::stdout().is_tty() && self.interactive.unwrap_or(true) + } + + pub(crate) fn should_proxy_build(&self) -> bool { + match self.build_arguments.platform() { + Platform::Server => true, + _ => self.build_arguments.fullstack, + } } } -impl Deref for Serve { - type Target = Build; +impl std::ops::Deref for ServeArgs { + type Target = BuildArgs; fn deref(&self) -> &Self::Target { &self.build_arguments diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs new file mode 100644 index 000000000..a4ab3123e --- /dev/null +++ b/packages/cli/src/cli/target.rs @@ -0,0 +1,52 @@ +use super::*; + +/// Information about the target to build +#[derive(Clone, Debug, Default, Deserialize, Parser)] +pub(crate) struct TargetArgs { + /// Build for nightly [default: false] + #[clap(long)] + pub(crate) nightly: bool, + + /// Build a example [default: ""] + #[clap(long)] + pub(crate) example: Option, + + /// Build a binary [default: ""] + #[clap(long)] + pub(crate) bin: Option, + + /// The package to build + #[clap(short, long)] + pub(crate) package: Option, + + /// Space separated list of features to activate + #[clap(long)] + pub(crate) features: Vec, + + /// The feature to use for the client in a fullstack app [default: "web"] + #[clap(long)] + pub(crate) client_features: Vec, + + /// The feature to use for the server in a fullstack app [default: "server"] + #[clap(long)] + pub(crate) server_features: Vec, + + /// Don't include the default features in the build + #[clap(long)] + pub(crate) no_default_features: bool, + + /// The architecture to build for [default: "native"] + /// + /// Can either be `arm | arm64 | x86 | x86_64 | native` + #[clap(long)] + pub(crate) arch: Option, + + /// Are we building for a device or just the simulator + /// If device is false, then we'll build for the simulator + #[clap(long)] + pub(crate) device: Option, + + /// Rustc platform triple + #[clap(long)] + pub(crate) target: Option, +} diff --git a/packages/cli/src/cli/translate.rs b/packages/cli/src/cli/translate.rs index fdaf37cf8..2d2847d81 100644 --- a/packages/cli/src/cli/translate.rs +++ b/packages/cli/src/cli/translate.rs @@ -1,35 +1,32 @@ -use std::{io::IsTerminal as _, process::exit}; - -use dioxus_rsx::{BodyNode, CallBody, TemplateBody}; - -use crate::TraceSrc; - use super::*; +use crate::{Result, TraceSrc}; +use dioxus_rsx::{BodyNode, CallBody, TemplateBody}; +use std::{io::IsTerminal as _, process::exit}; /// Translate some source file into Dioxus code #[derive(Clone, Debug, Parser)] #[clap(name = "translate")] -pub struct Translate { +pub(crate) struct Translate { /// Activate debug mode // short and long flags (-d, --debug) will be deduced from the field's name #[clap(short, long)] - pub component: bool, + pub(crate) component: bool, /// Input file #[clap(short, long)] - pub file: Option, + pub(crate) file: Option, /// Input file #[clap(short, long)] - pub raw: Option, + pub(crate) raw: Option, /// Output file, stdout if not present #[arg(short, long)] - pub output: Option, + pub(crate) output: Option, } impl Translate { - pub fn translate(self) -> Result<()> { + pub(crate) fn translate(self) -> Result<()> { // Get the right input for the translation let contents = determine_input(self.file, self.raw)?; @@ -85,7 +82,7 @@ fn write_svg_section(out: &mut String, svgs: Vec) { for (idx, icon) in svgs.into_iter().enumerate() { let raw = dioxus_autofmt::write_block_out(&CallBody::new(TemplateBody::new(vec![icon]))).unwrap(); - out.push_str("\n\n pub fn icon_"); + out.push_str("\n\n pub(crate) fn icon_"); out.push_str(&idx.to_string()); out.push_str("() -> Element {\n rsx! {"); indent_and_write(&raw, 2, out); @@ -122,7 +119,7 @@ fn determine_input(file: Option, raw: Option) -> Result // If neither exist, we try to read from stdin if std::io::stdin().is_terminal() { - return custom_error!("No input file, source, or stdin to translate from."); + return Err(anyhow::anyhow!("No input file, source, or stdin to translate from.").into()); } let mut buffer = String::new(); diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index dfa13bf09..cd1b0159f 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,678 +1,13 @@ -use clap::Parser; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fmt::Display; -use std::net::{IpAddr, SocketAddr}; -use std::path::PathBuf; -use std::str::FromStr; - -#[derive( - Copy, - Clone, - Hash, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Debug, - Default, - clap::ValueEnum, -)] -#[non_exhaustive] -pub enum Platform { - /// Targeting the web platform using WASM - #[clap(name = "web")] - #[serde(rename = "web")] - #[default] - Web, - - /// Targeting the desktop platform using Tao/Wry-based webview - #[clap(name = "desktop")] - #[serde(rename = "desktop")] - Desktop, - - /// Targeting the server platform using Axum and Dioxus-Fullstack - #[clap(name = "fullstack")] - #[serde(rename = "fullstack")] - Fullstack, - - /// Targeting the static generation platform using SSR and Dioxus-Fullstack - #[clap(name = "static-generation")] - #[serde(rename = "static-generation")] - StaticGeneration, - - /// Targeting the static generation platform using SSR and Dioxus-Fullstack - #[clap(name = "liveview")] - #[serde(rename = "liveview")] - Liveview, -} - -/// An error that occurs when a platform is not recognized -pub struct UnknownPlatformError; - -impl std::error::Error for UnknownPlatformError {} - -impl std::fmt::Debug for UnknownPlatformError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Unknown platform") - } -} -impl std::fmt::Display for UnknownPlatformError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Unknown platform") - } -} - -impl FromStr for Platform { - type Err = UnknownPlatformError; - - fn from_str(s: &str) -> Result { - match s { - "web" => Ok(Self::Web), - "desktop" => Ok(Self::Desktop), - "fullstack" => Ok(Self::Fullstack), - "static-generation" => Ok(Self::StaticGeneration), - "liveview" => Ok(Self::Liveview), - _ => Err(UnknownPlatformError), - } - } -} - -impl Display for Platform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let feature = self.feature_name(); - f.write_str(feature) - } -} - -impl Platform { - /// All platforms the dioxus CLI supports - pub const ALL: &'static [Self] = &[ - Platform::Web, - Platform::Desktop, - Platform::Fullstack, - Platform::StaticGeneration, - ]; - - /// Get the feature name for the platform in the dioxus crate - pub fn feature_name(&self) -> &str { - match self { - Platform::Web => "web", - Platform::Desktop => "desktop", - Platform::Fullstack => "fullstack", - Platform::StaticGeneration => "static-generation", - Platform::Liveview => "liveview", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DioxusConfig { - pub application: ApplicationConfig, - - #[serde(default)] - pub web: WebConfig, - - #[serde(default)] - pub desktop: DesktopConfig, - - #[serde(default)] - pub bundle: BundleConfig, -} - -impl Default for DioxusConfig { - fn default() -> Self { - let name = default_name(); - Self { - application: ApplicationConfig { - name: name.clone(), - default_platform: default_platform(), - out_dir: out_dir_default(), - asset_dir: asset_dir_default(), - - sub_package: None, - }, - web: WebConfig { - app: WebAppConfig { - title: default_title(), - base_path: None, - }, - proxy: vec![], - watcher: Default::default(), - resource: WebResourceConfig { - dev: WebDevResourceConfig { - style: vec![], - script: vec![], - }, - style: Some(vec![]), - script: Some(vec![]), - }, - https: WebHttpsConfig { - enabled: None, - mkcert: None, - key_path: None, - cert_path: None, - }, - pre_compress: true, - wasm_opt: Default::default(), - }, - desktop: DesktopConfig::default(), - bundle: BundleConfig { - identifier: Some(format!("io.github.{name}")), - publisher: Some(name), - ..Default::default() - }, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ApplicationConfig { - #[serde(default = "default_name")] - pub name: String, - - #[serde(default = "default_platform")] - pub default_platform: Platform, - - #[serde(default = "out_dir_default")] - pub out_dir: PathBuf, - - #[serde(default = "asset_dir_default")] - pub asset_dir: PathBuf, - - #[serde(default)] - pub sub_package: Option, -} - -fn default_name() -> String { - "my-cool-project".into() -} - -fn default_platform() -> Platform { - Platform::Web -} - -fn asset_dir_default() -> PathBuf { - PathBuf::from("public") -} - -fn out_dir_default() -> PathBuf { - PathBuf::from("dist") -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebConfig { - #[serde(default)] - pub app: WebAppConfig, - #[serde(default)] - pub proxy: Vec, - #[serde(default)] - pub watcher: WebWatcherConfig, - #[serde(default)] - pub resource: WebResourceConfig, - #[serde(default)] - pub https: WebHttpsConfig, - /// Whether to enable pre-compression of assets and wasm during a web build in release mode - #[serde(default = "true_bool")] - pub pre_compress: bool, - /// The wasm-opt configuration - #[serde(default)] - pub wasm_opt: WasmOptConfig, -} - -impl Default for WebConfig { - fn default() -> Self { - Self { - pre_compress: true_bool(), - app: Default::default(), - https: Default::default(), - wasm_opt: Default::default(), - proxy: Default::default(), - watcher: Default::default(), - resource: Default::default(), - } - } -} - -/// Represents configuration items for the desktop platform. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DesktopConfig { - /// Describes whether a debug-mode desktop app should be always-on-top. - #[serde(default)] - pub always_on_top: bool, -} - -impl Default for DesktopConfig { - fn default() -> Self { - Self { - always_on_top: true, - } - } -} - -/// The wasm-opt configuration -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct WasmOptConfig { - /// The wasm-opt level to use for release builds [default: s] - /// Options: - /// - z: optimize aggressively for size - /// - s: optimize for size - /// - 1: optimize for speed - /// - 2: optimize for more for speed - /// - 3: optimize for even more for speed - /// - 4: optimize aggressively for speed - #[serde(default)] - pub level: WasmOptLevel, - - /// Keep debug symbols in the wasm file - #[serde(default = "false_bool")] - pub debug: bool, -} - -/// The wasm-opt level to use for release web builds [default: 4] -#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] -pub enum WasmOptLevel { - /// Optimize aggressively for size - #[serde(rename = "z")] - Z, - /// Optimize for size - #[serde(rename = "s")] - S, - /// Don't optimize - #[serde(rename = "0")] - Zero, - /// Optimize for speed - #[serde(rename = "1")] - One, - /// Optimize for more for speed - #[serde(rename = "2")] - Two, - /// Optimize for even more for speed - #[serde(rename = "3")] - Three, - /// Optimize aggressively for speed - #[serde(rename = "4")] - #[default] - Four, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebAppConfig { - #[serde(default = "default_title")] - pub title: String, - pub base_path: Option, -} - -impl WebAppConfig { - /// Get the normalized base path for the application with `/` trimmed from both ends. If the base path is not set, this will return `.`. - pub fn base_path(&self) -> &str { - match &self.base_path { - Some(path) => path.trim_matches('/'), - None => ".", - } - } -} - -impl Default for WebAppConfig { - fn default() -> Self { - Self { - title: default_title(), - base_path: None, - } - } -} - -fn default_title() -> String { - "dioxus | ⛺".into() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebProxyConfig { - pub backend: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebWatcherConfig { - #[serde(default = "watch_path_default")] - pub watch_path: Vec, - - #[serde(default)] - pub reload_html: bool, - - #[serde(default = "true_bool")] - pub index_on_404: bool, -} - -impl Default for WebWatcherConfig { - fn default() -> Self { - Self { - watch_path: watch_path_default(), - reload_html: false, - index_on_404: true, - } - } -} - -fn watch_path_default() -> Vec { - vec![PathBuf::from("src"), PathBuf::from("examples")] -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct WebResourceConfig { - pub dev: WebDevResourceConfig, - pub style: Option>, - pub script: Option>, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct WebDevResourceConfig { - #[serde(default)] - pub style: Vec, - #[serde(default)] - pub script: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct WebHttpsConfig { - pub enabled: Option, - pub mkcert: Option, - pub key_path: Option, - pub cert_path: Option, -} - -fn true_bool() -> bool { - true -} - -fn false_bool() -> bool { - false -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BundleConfig { - pub identifier: Option, - pub publisher: Option, - pub icon: Option>, - pub resources: Option>, - pub copyright: Option, - pub category: Option, - pub short_description: Option, - pub long_description: Option, - pub external_bin: Option>, - pub deb: Option, - pub macos: Option, - pub windows: Option, -} - -impl From for tauri_bundler::BundleSettings { - fn from(val: BundleConfig) -> Self { - tauri_bundler::BundleSettings { - identifier: val.identifier, - publisher: val.publisher, - icon: val.icon, - resources: val.resources, - copyright: val.copyright, - category: val.category.and_then(|c| c.parse().ok()), - short_description: val.short_description, - long_description: val.long_description, - external_bin: val.external_bin, - deb: val.deb.map(Into::into).unwrap_or_default(), - macos: val.macos.map(Into::into).unwrap_or_default(), - windows: val.windows.map(Into::into).unwrap_or_default(), - ..Default::default() - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DebianSettings { - pub depends: Option>, - pub files: HashMap, - pub nsis: Option, -} - -impl From for tauri_bundler::DebianSettings { - fn from(val: DebianSettings) -> Self { - tauri_bundler::DebianSettings { - depends: val.depends, - files: val.files, - desktop_template: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct WixSettings { - pub language: Vec<(String, Option)>, - pub template: Option, - pub fragment_paths: Vec, - pub component_group_refs: Vec, - pub component_refs: Vec, - pub feature_group_refs: Vec, - pub feature_refs: Vec, - pub merge_refs: Vec, - pub skip_webview_install: bool, - pub license: Option, - pub enable_elevated_update_task: bool, - pub banner_path: Option, - pub dialog_image_path: Option, - pub fips_compliant: bool, -} - -impl From for tauri_bundler::WixSettings { - fn from(val: WixSettings) -> Self { - tauri_bundler::WixSettings { - language: tauri_bundler::bundle::WixLanguage({ - let mut languages: Vec<_> = val - .language - .iter() - .map(|l| { - ( - l.0.clone(), - tauri_bundler::bundle::WixLanguageConfig { - locale_path: l.1.clone(), - }, - ) - }) - .collect(); - if languages.is_empty() { - languages.push(("en-US".into(), Default::default())); - } - languages - }), - template: val.template, - fragment_paths: val.fragment_paths, - component_group_refs: val.component_group_refs, - component_refs: val.component_refs, - feature_group_refs: val.feature_group_refs, - feature_refs: val.feature_refs, - merge_refs: val.merge_refs, - skip_webview_install: val.skip_webview_install, - license: val.license, - enable_elevated_update_task: val.enable_elevated_update_task, - banner_path: val.banner_path, - dialog_image_path: val.dialog_image_path, - fips_compliant: val.fips_compliant, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct MacOsSettings { - pub frameworks: Option>, - pub minimum_system_version: Option, - pub license: Option, - pub exception_domain: Option, - pub signing_identity: Option, - pub provider_short_name: Option, - pub entitlements: Option, - pub info_plist_path: Option, -} - -impl From for tauri_bundler::MacOsSettings { - fn from(val: MacOsSettings) -> Self { - tauri_bundler::MacOsSettings { - frameworks: val.frameworks, - minimum_system_version: val.minimum_system_version, - license: val.license, - exception_domain: val.exception_domain, - signing_identity: val.signing_identity, - provider_short_name: val.provider_short_name, - entitlements: val.entitlements, - info_plist_path: val.info_plist_path, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WindowsSettings { - pub digest_algorithm: Option, - pub certificate_thumbprint: Option, - pub timestamp_url: Option, - pub tsp: bool, - pub wix: Option, - pub icon_path: Option, - pub webview_install_mode: WebviewInstallMode, - pub webview_fixed_runtime_path: Option, - pub allow_downgrades: bool, - pub nsis: Option, -} - -impl From for tauri_bundler::WindowsSettings { - fn from(val: WindowsSettings) -> Self { - tauri_bundler::WindowsSettings { - digest_algorithm: val.digest_algorithm, - certificate_thumbprint: val.certificate_thumbprint, - timestamp_url: val.timestamp_url, - tsp: val.tsp, - wix: val.wix.map(Into::into), - icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), - webview_install_mode: val.webview_install_mode.into(), - webview_fixed_runtime_path: val.webview_fixed_runtime_path, - allow_downgrades: val.allow_downgrades, - nsis: val.nsis.map(Into::into), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NsisSettings { - pub template: Option, - pub license: Option, - pub header_image: Option, - pub sidebar_image: Option, - pub installer_icon: Option, - pub install_mode: NSISInstallerMode, - pub languages: Option>, - pub custom_language_files: Option>, - pub display_language_selector: bool, -} - -impl From for tauri_bundler::NsisSettings { - fn from(val: NsisSettings) -> Self { - tauri_bundler::NsisSettings { - license: val.license, - header_image: val.header_image, - sidebar_image: val.sidebar_image, - installer_icon: val.installer_icon, - install_mode: val.install_mode.into(), - languages: val.languages, - display_language_selector: val.display_language_selector, - custom_language_files: None, - template: None, - compression: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum NSISInstallerMode { - CurrentUser, - PerMachine, - Both, -} - -impl From for tauri_utils::config::NSISInstallerMode { - fn from(val: NSISInstallerMode) -> Self { - match val { - NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, - NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, - NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum WebviewInstallMode { - Skip, - DownloadBootstrapper { silent: bool }, - EmbedBootstrapper { silent: bool }, - OfflineInstaller { silent: bool }, - FixedRuntime { path: PathBuf }, -} - -impl WebviewInstallMode { - fn into(self) -> tauri_utils::config::WebviewInstallMode { - match self { - Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, - Self::DownloadBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } - } - Self::EmbedBootstrapper { silent } => { - tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } - } - Self::OfflineInstaller { silent } => { - tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } - } - Self::FixedRuntime { path } => { - tauri_utils::config::WebviewInstallMode::FixedRuntime { path } - } - } - } -} - -impl Default for WebviewInstallMode { - fn default() -> Self { - Self::OfflineInstaller { silent: false } - } -} - -/// The arguments for the address the server will run on - -#[derive(Clone, Debug, Parser)] -pub struct AddressArguments { - /// The port the server will run on - #[clap(long)] - #[clap(default_value_t = default_port())] - pub port: u16, - - /// The address the server will run on - #[clap(long, default_value_t = default_address())] - pub addr: std::net::IpAddr, -} - -impl Default for AddressArguments { - fn default() -> Self { - Self { - port: default_port(), - addr: default_address(), - } - } -} - -impl AddressArguments { - /// Get the address the server should run on - pub fn address(&self) -> SocketAddr { - SocketAddr::new(self.addr, self.port) - } -} - -fn default_port() -> u16 { - 8080 -} - -fn default_address() -> IpAddr { - IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)) -} +mod app; +mod bundle; +mod desktop; +mod dioxus_config; +mod serve; +mod web; + +pub(crate) use app::*; +pub(crate) use bundle::*; +pub(crate) use desktop::*; +pub(crate) use dioxus_config::*; +pub(crate) use serve::*; +pub(crate) use web::*; diff --git a/packages/cli/src/config/app.rs b/packages/cli/src/config/app.rs new file mode 100644 index 000000000..ea426e868 --- /dev/null +++ b/packages/cli/src/config/app.rs @@ -0,0 +1,23 @@ +use crate::Platform; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ApplicationConfig { + #[serde(default = "default_platform")] + pub(crate) default_platform: Platform, + + #[serde(default = "asset_dir_default")] + pub(crate) asset_dir: PathBuf, + + #[serde(default)] + pub(crate) sub_package: Option, +} + +pub(crate) fn default_platform() -> Platform { + Platform::Web +} + +pub(crate) fn asset_dir_default() -> PathBuf { + PathBuf::from("assets") +} diff --git a/packages/cli/src/config/bundle.rs b/packages/cli/src/config/bundle.rs new file mode 100644 index 000000000..bed8eef8f --- /dev/null +++ b/packages/cli/src/config/bundle.rs @@ -0,0 +1,204 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct BundleConfig { + pub(crate) identifier: Option, + pub(crate) publisher: Option, + pub(crate) icon: Option>, + pub(crate) resources: Option>, + pub(crate) copyright: Option, + pub(crate) category: Option, + pub(crate) short_description: Option, + pub(crate) long_description: Option, + pub(crate) external_bin: Option>, + pub(crate) deb: Option, + pub(crate) macos: Option, + pub(crate) windows: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct DebianSettings { + // OS-specific settings: + /// the list of debian dependencies. + pub depends: Option>, + /// the list of dependencies the package provides. + pub provides: Option>, + /// the list of package conflicts. + pub conflicts: Option>, + /// the list of package replaces. + pub replaces: Option>, + /// List of custom files to add to the deb package. + /// Maps the path on the debian package to the path of the file to include (relative to the current working directory). + pub files: HashMap, + /// Path to a custom desktop file Handlebars template. + /// + /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. + pub desktop_template: Option, + /// Define the section in Debian Control file. See : + pub section: Option, + /// Change the priority of the Debian Package. By default, it is set to `optional`. + /// Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra` + pub priority: Option, + /// Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See + /// + pub changelog: Option, + /// Path to script that will be executed before the package is unpacked. See + /// + pub pre_install_script: Option, + /// Path to script that will be executed after the package is unpacked. See + /// + pub post_install_script: Option, + /// Path to script that will be executed before the package is removed. See + /// + pub pre_remove_script: Option, + /// Path to script that will be executed after the package is removed. See + /// + pub post_remove_script: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct WixSettings { + pub(crate) language: Vec<(String, Option)>, + pub(crate) template: Option, + pub(crate) fragment_paths: Vec, + pub(crate) component_group_refs: Vec, + pub(crate) component_refs: Vec, + pub(crate) feature_group_refs: Vec, + pub(crate) feature_refs: Vec, + pub(crate) merge_refs: Vec, + pub(crate) skip_webview_install: bool, + pub(crate) license: Option, + pub(crate) enable_elevated_update_task: bool, + pub(crate) banner_path: Option, + pub(crate) dialog_image_path: Option, + pub(crate) fips_compliant: bool, + /// MSI installer version in the format `major.minor.patch.build` (build is optional). + /// + /// Because a valid version is required for MSI installer, it will be derived from [`PackageSettings::version`] if this field is not set. + /// + /// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. + /// The third and fourth fields have a maximum value of 65,535. + /// + /// See for more info. + pub version: Option, + /// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, + /// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app. + /// + /// By default, tauri generates this code by generating a Uuid v5 using the string `.exe.app.x64` in the DNS namespace. + /// You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`. + /// + /// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code + /// whenever you want to change your product name. + pub upgrade_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct MacOsSettings { + pub(crate) frameworks: Option>, + pub(crate) minimum_system_version: Option, + pub(crate) license: Option, + pub(crate) exception_domain: Option, + pub(crate) signing_identity: Option, + pub(crate) provider_short_name: Option, + pub(crate) entitlements: Option, + pub(crate) info_plist_path: Option, + /// List of custom files to add to the application bundle. + /// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory). + pub files: HashMap, + /// Preserve the hardened runtime version flag, see + /// + /// Settings this to `false` is useful when using an ad-hoc signature, making it less strict. + pub hardened_runtime: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WindowsSettings { + pub(crate) digest_algorithm: Option, + pub(crate) certificate_thumbprint: Option, + pub(crate) timestamp_url: Option, + pub(crate) tsp: bool, + pub(crate) wix: Option, + pub(crate) icon_path: Option, + pub(crate) webview_install_mode: WebviewInstallMode, + pub(crate) webview_fixed_runtime_path: Option, + pub(crate) allow_downgrades: bool, + pub(crate) nsis: Option, + /// Specify a custom command to sign the binaries. + /// This command needs to have a `%1` in it which is just a placeholder for the binary path, + /// which we will detect and replace before calling the command. + /// + /// Example: + /// ```text + /// sign-cli --arg1 --arg2 %1 + /// ``` + /// + /// By Default we use `signtool.exe` which can be found only on Windows so + /// if you are on another platform and want to cross-compile and sign you will + /// need to use another tool like `osslsigncode`. + pub sign_command: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NsisSettings { + pub(crate) template: Option, + pub(crate) license: Option, + pub(crate) header_image: Option, + pub(crate) sidebar_image: Option, + pub(crate) installer_icon: Option, + pub(crate) install_mode: NSISInstallerMode, + pub(crate) languages: Option>, + pub(crate) custom_language_files: Option>, + pub(crate) display_language_selector: bool, + pub(crate) start_menu_folder: Option, + pub(crate) installer_hooks: Option, + /// Try to ensure that the WebView2 version is equal to or newer than this version, + /// if the user's WebView2 is older than this version, + /// the installer will try to trigger a WebView2 update. + pub minimum_webview2_version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum NSISInstallerMode { + CurrentUser, + PerMachine, + Both, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum WebviewInstallMode { + Skip, + DownloadBootstrapper { silent: bool }, + EmbedBootstrapper { silent: bool }, + OfflineInstaller { silent: bool }, + FixedRuntime { path: PathBuf }, +} + +impl Default for WebviewInstallMode { + fn default() -> Self { + Self::OfflineInstaller { silent: false } + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum PackageType { + MacOsBundle, + IosBundle, + WindowsMsi, + Deb, + Rpm, + AppImage, + Dmg, + Updater, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomSignCommandSettings { + /// The command to run to sign the binary. + pub cmd: String, + /// The arguments to pass to the command. + /// + /// "%1" will be replaced with the path to the binary to be signed. + pub args: Vec, +} diff --git a/packages/cli/src/config/desktop.rs b/packages/cli/src/config/desktop.rs new file mode 100644 index 000000000..f8b8dd797 --- /dev/null +++ b/packages/cli/src/config/desktop.rs @@ -0,0 +1,5 @@ +use serde::{Deserialize, Serialize}; + +/// Represents configuration items for the desktop platform. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub(crate) struct DesktopConfig {} diff --git a/packages/cli/src/config/dioxus_config.rs b/packages/cli/src/config/dioxus_config.rs new file mode 100644 index 000000000..23f96b4ab --- /dev/null +++ b/packages/cli/src/config/dioxus_config.rs @@ -0,0 +1,105 @@ +use super::*; +use crate::Result; +use anyhow::Context; +use krates::{Krates, NodeId}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct DioxusConfig { + pub(crate) application: ApplicationConfig, + + #[serde(default)] + pub(crate) web: WebConfig, + + #[serde(default)] + pub(crate) desktop: DesktopConfig, + + #[serde(default)] + pub(crate) bundle: BundleConfig, +} + +impl Default for DioxusConfig { + fn default() -> Self { + Self { + application: ApplicationConfig { + default_platform: default_platform(), + asset_dir: asset_dir_default(), + sub_package: None, + }, + web: WebConfig { + app: WebAppConfig { + title: default_title(), + base_path: None, + }, + proxy: vec![], + watcher: Default::default(), + resource: WebResourceConfig { + dev: WebDevResourceConfig { + style: vec![], + script: vec![], + }, + style: Some(vec![]), + script: Some(vec![]), + }, + https: WebHttpsConfig { + enabled: None, + mkcert: None, + key_path: None, + cert_path: None, + }, + pre_compress: true, + wasm_opt: Default::default(), + }, + desktop: DesktopConfig::default(), + bundle: BundleConfig::default(), + } + } +} + +impl DioxusConfig { + pub fn load(krates: &Krates, package: NodeId) -> Result> { + // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml + let mut current_dir = krates[package] + .manifest_path + .parent() + .unwrap() + .as_std_path() + .to_path_buf() + .canonicalize()?; + + let workspace_path = krates + .workspace_root() + .as_std_path() + .to_path_buf() + .canonicalize()?; + + let mut dioxus_conf_file = None; + while current_dir.starts_with(&workspace_path) { + let config = ["Dioxus.toml", "dioxus.toml"] + .into_iter() + .map(|file| current_dir.join(file)) + .find(|path| path.is_file()); + + // Try to find Dioxus.toml in the current directory + if let Some(new_config) = config { + dioxus_conf_file = Some(new_config.as_path().to_path_buf()); + break; + } + // If we can't find it, go up a directory + current_dir = current_dir + .parent() + .context("Failed to find Dioxus.toml")? + .to_path_buf(); + } + + let Some(dioxus_conf_file) = dioxus_conf_file else { + return Ok(None); + }; + + toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) + .map_err(|err| { + anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into() + }) + .map(Some) + } +} diff --git a/packages/cli/src/config/serve.rs b/packages/cli/src/config/serve.rs new file mode 100644 index 000000000..6758ffd8e --- /dev/null +++ b/packages/cli/src/config/serve.rs @@ -0,0 +1,34 @@ +#![allow(unused)] // lots of configs... + +use clap::Parser; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; + +/// The arguments for the address the server will run on +#[derive(Clone, Debug, Parser)] +pub(crate) struct AddressArguments { + /// The port the server will run on + #[clap(long)] + #[clap(default_value_t = default_port())] + pub(crate) port: u16, + + /// The address the server will run on + #[clap(long, default_value_t = default_address())] + pub(crate) addr: std::net::IpAddr, +} + +impl Default for AddressArguments { + fn default() -> Self { + Self { + port: default_port(), + addr: default_address(), + } + } +} + +fn default_port() -> u16 { + 8080 +} + +fn default_address() -> IpAddr { + IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)) +} diff --git a/packages/cli/src/config/web.rs b/packages/cli/src/config/web.rs new file mode 100644 index 000000000..489bfa07c --- /dev/null +++ b/packages/cli/src/config/web.rs @@ -0,0 +1,180 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebConfig { + #[serde(default)] + pub(crate) app: WebAppConfig, + + #[serde(default)] + pub(crate) proxy: Vec, + + #[serde(default)] + pub(crate) watcher: WebWatcherConfig, + + #[serde(default)] + pub(crate) resource: WebResourceConfig, + + #[serde(default)] + pub(crate) https: WebHttpsConfig, + + /// Whether to enable pre-compression of assets and wasm during a web build in release mode + #[serde(default = "true_bool")] + pub(crate) pre_compress: bool, + + /// The wasm-opt configuration + #[serde(default)] + pub(crate) wasm_opt: WasmOptConfig, +} + +impl Default for WebConfig { + fn default() -> Self { + Self { + pre_compress: true_bool(), + app: Default::default(), + https: Default::default(), + wasm_opt: Default::default(), + proxy: Default::default(), + watcher: Default::default(), + resource: Default::default(), + } + } +} + +/// The wasm-opt configuration +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct WasmOptConfig { + /// The wasm-opt level to use for release builds [default: s] + /// Options: + /// - z: optimize aggressively for size + /// - s: optimize for size + /// - 1: optimize for speed + /// - 2: optimize for more for speed + /// - 3: optimize for even more for speed + /// - 4: optimize aggressively for speed + #[serde(default)] + pub(crate) level: WasmOptLevel, + + /// Keep debug symbols in the wasm file + #[serde(default = "false_bool")] + pub(crate) debug: bool, +} + +/// The wasm-opt level to use for release web builds [default: 4] +#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] +pub(crate) enum WasmOptLevel { + /// Optimize aggressively for size + #[serde(rename = "z")] + Z, + /// Optimize for size + #[serde(rename = "s")] + S, + /// Don't optimize + #[serde(rename = "0")] + Zero, + /// Optimize for speed + #[serde(rename = "1")] + One, + /// Optimize for more for speed + #[serde(rename = "2")] + Two, + /// Optimize for even more for speed + #[serde(rename = "3")] + Three, + /// Optimize aggressively for speed + #[serde(rename = "4")] + #[default] + Four, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebAppConfig { + #[serde(default = "default_title")] + pub(crate) title: String, + pub(crate) base_path: Option, +} + +impl WebAppConfig { + /// Get the normalized base path for the application with `/` trimmed from both ends. If the base path is not set, this will return `.`. + pub(crate) fn base_path(&self) -> &str { + match &self.base_path { + Some(path) => path.trim_matches('/'), + None => ".", + } + } +} + +impl Default for WebAppConfig { + fn default() -> Self { + Self { + title: default_title(), + base_path: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebProxyConfig { + pub(crate) backend: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebWatcherConfig { + #[serde(default = "watch_path_default")] + pub(crate) watch_path: Vec, + + #[serde(default)] + pub(crate) reload_html: bool, + + #[serde(default = "true_bool")] + pub(crate) index_on_404: bool, +} + +impl Default for WebWatcherConfig { + fn default() -> Self { + Self { + watch_path: watch_path_default(), + reload_html: false, + index_on_404: true, + } + } +} + +fn watch_path_default() -> Vec { + vec![PathBuf::from("src"), PathBuf::from("examples")] +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebResourceConfig { + pub(crate) dev: WebDevResourceConfig, + pub(crate) style: Option>, + pub(crate) script: Option>, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WebDevResourceConfig { + #[serde(default)] + pub(crate) style: Vec, + #[serde(default)] + pub(crate) script: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub(crate) struct WebHttpsConfig { + pub(crate) enabled: Option, + pub(crate) mkcert: Option, + pub(crate) key_path: Option, + pub(crate) cert_path: Option, +} + +fn true_bool() -> bool { + true +} + +fn false_bool() -> bool { + false +} + +pub(crate) fn default_title() -> String { + "dioxus | ⛺".into() +} diff --git a/packages/cli/src/dioxus_crate.rs b/packages/cli/src/dioxus_crate.rs index ad727f329..631b36526 100644 --- a/packages/cli/src/dioxus_crate.rs +++ b/packages/cli/src/dioxus_crate.rs @@ -1,86 +1,567 @@ -use crate::build::TargetArgs; -use crate::config::{DioxusConfig, Platform}; +use crate::CliSettings; +use crate::{config::DioxusConfig, TargetArgs}; +use crate::{Platform, Result}; +use anyhow::Context; use krates::{cm::Target, KrateDetails}; use krates::{cm::TargetKind, Cmd, Krates, NodeId}; -use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use std::sync::Arc; -use std::{ - fmt::{Display, Formatter}, - path::PathBuf, -}; +use std::{io::Write, path::Path}; +use toml_edit::Item; -use crate::metadata::CargoError; +// Contains information about the crate we are currently in and the dioxus config for that crate +#[derive(Clone)] +pub(crate) struct DioxusCrate { + pub(crate) krates: Arc, + pub(crate) package: NodeId, + pub(crate) config: DioxusConfig, + pub(crate) target: Target, + pub(crate) settings: CliSettings, +} -/// Load the dioxus config from a path -fn load_dioxus_config( - krates: &Krates, - package: NodeId, -) -> Result, CrateConfigError> { - fn acquire_dioxus_toml(dir: &std::path::Path) -> Option { - ["Dioxus.toml", "dioxus.toml"] - .into_iter() - .map(|file| dir.join(file)) - .find(|path| path.is_file()) - } +pub(crate) static PROFILE_WASM: &str = "dioxus-wasm"; +pub(crate) static PROFILE_ANDROID: &str = "dioxus-android"; +pub(crate) static PROFILE_SERVER: &str = "dioxus-server"; - // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml - let mut current_dir = krates[package] - .manifest_path - .parent() - .unwrap() - .as_std_path() - .to_path_buf() - .canonicalize()?; - let workspace_path = krates - .workspace_root() - .as_std_path() - .to_path_buf() - .canonicalize()?; +impl DioxusCrate { + pub(crate) fn new(target: &TargetArgs) -> Result { + let mut cmd = Cmd::new(); + cmd.features(target.features.clone()); - let mut dioxus_conf_file = None; - while current_dir.starts_with(&workspace_path) { - // Try to find Dioxus.toml in the current directory - if let Some(new_config) = acquire_dioxus_toml(¤t_dir) { - dioxus_conf_file = Some(new_config.as_path().to_path_buf()); - break; - } - // If we can't find it, go up a directory - current_dir = current_dir - .parent() - .ok_or(CrateConfigError::CurrentPackageNotFound)? - .to_path_buf(); - } + let krates = krates::Builder::new() + .build(cmd, |_| {}) + .context("Failed to run cargo metadata")?; - let Some(dioxus_conf_file) = dioxus_conf_file else { - return Ok(None); - }; + let package = find_main_package(&krates, target.package.clone())?; - let cfg = toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) - .map_err(|err| { - CrateConfigError::LoadDioxusConfig(LoadDioxusConfigError { - location: dioxus_conf_file.display().to_string(), - error: err.to_string(), + let dioxus_config = DioxusConfig::load(&krates, package)?.unwrap_or_default(); + + let package_name = krates[package].name.clone(); + let target_kind = if target.example.is_some() { + TargetKind::Example + } else { + TargetKind::Bin + }; + + let target_name = target + .example + .clone() + .or(target.bin.clone()) + .unwrap_or(package_name); + + let main_package = &krates[package]; + let target = main_package + .targets + .iter() + .find(|target| { + target_name == target.name.as_str() && target.kind.contains(&target_kind) }) + .with_context(|| format!("Failed to find target {target_name}"))? + .clone(); + + let settings = CliSettings::load(); + + Ok(Self { + krates: Arc::new(krates), + package, + config: dioxus_config, + target, + settings, }) - .map(Some); - match cfg { - Ok(Some(mut cfg)) => { - let name = cfg.application.name.clone(); - if cfg.bundle.identifier.is_none() { - cfg.bundle.identifier = Some(format!("io.github.{name}")); + } + + /// Compose an asset directory. Represents the typical "public" directory + /// with publicly available resources (configurable in the `Dioxus.toml`). + pub(crate) fn legacy_asset_dir(&self) -> PathBuf { + self.crate_dir().join(&self.config.application.asset_dir) + } + + /// Get the list of files in the "legacy" asset directory + pub(crate) fn legacy_asset_dir_files(&self) -> Vec { + let mut files = vec![]; + + let Ok(read_dir) = self.legacy_asset_dir().read_dir() else { + return files; + }; + + for entry in read_dir.flatten() { + files.push(entry.path()); + } + + files + } + + /// Compose an out directory. Represents the typical "dist" directory that + /// is "distributed" after building an application (configurable in the + /// `Dioxus.toml`). + fn out_dir(&self) -> PathBuf { + let dir = self.workspace_dir().join("target").join("dx"); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + /// Create a workdir for the given platform + /// This can be used as a temporary directory for the build, but in an observable way such that + /// you can see the files in the directory via `target` + /// + /// target/dx/build/app/web/ + /// target/dx/build/app/web/public/ + /// target/dx/build/app/web/server.exe + pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf { + self.out_dir() + .join(self.executable_name()) + .join(if release { "release" } else { "debug" }) + .join(platform.build_folder_name()) + } + + /// target/dx/bundle/app/ + /// target/dx/bundle/app/blah.app + /// target/dx/bundle/app/blah.exe + /// target/dx/bundle/app/public/ + pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf { + self.out_dir() + .join(self.executable_name()) + .join("bundle") + .join(platform.build_folder_name()) + } + + /// Get the workspace directory for the crate + pub(crate) fn workspace_dir(&self) -> PathBuf { + self.krates.workspace_root().as_std_path().to_path_buf() + } + + /// Get the directory of the crate + pub(crate) fn crate_dir(&self) -> PathBuf { + self.package() + .manifest_path + .parent() + .unwrap() + .as_std_path() + .to_path_buf() + } + + /// Get the main source file of the target + pub(crate) fn main_source_file(&self) -> PathBuf { + self.target.src_path.as_std_path().to_path_buf() + } + + /// Get the package we are currently in + pub(crate) fn package(&self) -> &krates::cm::Package { + &self.krates[self.package] + } + + /// Get the name of the package we are compiling + pub(crate) fn executable_name(&self) -> &str { + &self.target.name + } + + /// Get the type of executable we are compiling + pub(crate) fn executable_type(&self) -> krates::cm::TargetKind { + self.target.kind[0].clone() + } + + /// Try to autodetect the platform from the package by reading its features + /// + /// Read the default-features list and/or the features list on dioxus to see if we can autodetect the platform + pub(crate) fn autodetect_platform(&self) -> Option<(Platform, String)> { + let krate = self.krates.krates_by_name("dioxus").next()?; + + // We're going to accumulate the platforms that are enabled + // This will let us create a better warning if multiple platforms are enabled + let manually_enabled_platforms = self + .krates + .get_enabled_features(krate.kid)? + .iter() + .flat_map(|feature| { + Platform::autodetect_from_cargo_feature(feature).map(|f| (f, feature.to_string())) + }) + .collect::>(); + + if manually_enabled_platforms.len() > 1 { + tracing::error!("Multiple platforms are enabled. Please specify a platform with `--platform ` or set a single default platform using a cargo feature."); + for platform in manually_enabled_platforms { + tracing::error!(" - {platform:?}"); } - if cfg.bundle.publisher.is_none() { - cfg.bundle.publisher = Some(name); + return None; + } + + if manually_enabled_platforms.len() == 1 { + return manually_enabled_platforms.first().cloned(); + } + + // Let's try and find the list of platforms from the feature list + // This lets apps that specify web + server to work without specifying the platform. + // This is because we treat `server` as a binary thing rather than a dedicated platform, so at least we can disambiguate it + let possible_platforms = self + .package() + .features + .iter() + .filter_map(|(feature, _features)| { + match Platform::autodetect_from_cargo_feature(feature) { + Some(platform) => Some((platform, feature.to_string())), + None => { + let auto_implicit = _features + .iter() + .filter_map(|f| { + if !f.starts_with("dioxus?/") && !f.starts_with("dioxus/") { + return None; + } + + let rest = f + .trim_start_matches("dioxus/") + .trim_start_matches("dioxus?/"); + + Platform::autodetect_from_cargo_feature(rest) + }) + .collect::>(); + + if auto_implicit.len() == 1 { + Some((auto_implicit.first().copied().unwrap(), feature.to_string())) + } else { + None + } + } + } + }) + .filter(|platform| platform.0 != Platform::Server) + .collect::>(); + + if possible_platforms.len() == 1 { + return possible_platforms.first().cloned(); + } + + tracing::warn!("Could not autodetect platform. Platform must be explicitly specified. Pass `--platform ` or set a default platform using a cargo feature."); + + None + } + + /// Check if dioxus is being built with a particular feature + pub(crate) fn has_dioxus_feature(&self, filter: &str) -> bool { + self.krates.krates_by_name("dioxus").any(|dioxus| { + self.krates + .get_enabled_features(dioxus.kid) + .map(|features| features.contains(filter)) + .unwrap_or_default() + }) + } + + /// Get the features required to build for the given platform + pub(crate) fn feature_for_platform(&self, platform: Platform) -> Option { + let package = self.package(); + + // Try to find the feature that activates the dioxus feature for the given platform + let dioxus_feature = platform.feature_name(); + + package.features.iter().find_map(|(key, features)| { + // if the feature is just the name of the platform, we use that + if key == dioxus_feature { + return Some(key.clone()); } - Ok(Some(cfg)) + // Otherwise look for the feature that starts with dioxus/ or dioxus?/ and matches the platform + for feature in features { + if let Some((_, after_dioxus)) = feature.split_once("dioxus") { + if let Some(dioxus_feature_enabled) = + after_dioxus.trim_start_matches('?').strip_prefix('/') + { + // If that enables the feature we are looking for, return that feature + if dioxus_feature_enabled == dioxus_feature { + return Some(key.clone()); + } + } + } + } + None + }) + } + + /// Check if assets should be pre_compressed. This will only be true in release mode if the user + /// has enabled pre_compress in the web config. + pub(crate) fn should_pre_compress_web_assets(&self, release: bool) -> bool { + self.config.web.pre_compress && release + } + + // The `opt-level=2` increases build times, but can noticeably decrease time + // between saving changes and being able to interact with an app (for wasm/web). The "overall" + // time difference (between having and not having the optimization) can be + // almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends + // on setup (hardware, OS, browser, idle load). + // + // Find or create the client and server profiles in the .cargo/config.toml file + pub(crate) fn initialize_profiles(&self) -> crate::Result<()> { + let config_path = self.workspace_dir().join(".cargo/config.toml"); + let mut config = match std::fs::read_to_string(&config_path) { + Ok(config) => config.parse::().map_err(|e| { + crate::Error::Other(anyhow::anyhow!("Failed to parse .cargo/config.toml: {}", e)) + })?, + Err(_) => Default::default(), + }; + + if let Item::Table(table) = config + .as_table_mut() + .entry("profile") + .or_insert(Item::Table(Default::default())) + { + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_WASM) { + let mut client = toml_edit::Table::new(); + client.insert("inherits", Item::Value("dev".into())); + client.insert("opt-level", Item::Value(2.into())); + entry.insert(Item::Table(client)); + } + + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_SERVER) { + let mut server = toml_edit::Table::new(); + server.insert("inherits", Item::Value("dev".into())); + server.insert("opt-level", Item::Value(2.into())); + entry.insert(Item::Table(server)); + } + + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_ANDROID) { + let mut android = toml_edit::Table::new(); + android.insert("inherits", Item::Value("dev".into())); + android.insert("opt-level", Item::Value(2.into())); + entry.insert(Item::Table(android)); + } } - cfg => cfg, + + // Write the config back to the file + if let Some(parent) = config_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = std::fs::File::create(config_path)?; + let mut buf_writer = std::io::BufWriter::new(file); + write!(buf_writer, "{}", config)?; + + Ok(()) + } + + fn default_ignore_list(&self) -> Vec<&'static str> { + vec![ + ".git", + ".github", + ".vscode", + "target", + "node_modules", + "dist", + "*~", + ".*", + "*.lock", + "*.log", + "*.rs", + ] + } + + /// Create a new gitignore map for this target crate + /// + /// todo(jon): this is a bit expensive to build, so maybe we should cache it? + pub fn workspace_gitignore(&self) -> ignore::gitignore::Gitignore { + let crate_dir = self.crate_dir(); + + let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(&crate_dir); + ignore_builder.add(crate_dir.join(".gitignore")); + + let workspace_dir = self.workspace_dir(); + ignore_builder.add(workspace_dir.join(".gitignore")); + + for path in self.default_ignore_list() { + ignore_builder + .add_line(None, path) + .expect("failed to add path to file excluder"); + } + + ignore_builder.build().unwrap() + } + + /// Return the version of the wasm-bindgen crate if it exists + pub fn wasm_bindgen_version(&self) -> Option { + self.krates + .krates_by_name("wasm-bindgen") + .next() + .map(|krate| krate.krate.version.to_string()) + } + + pub(crate) fn default_platform(&self) -> Option { + let default = self.package().features.get("default")?; + + // we only trace features 1 level deep.. + for feature in default.iter() { + // If the user directly specified a platform we can just use that. + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + let auto = Platform::autodetect_from_cargo_feature(dx_feature); + if auto.is_some() { + return auto; + } + } + + // If the user is specifying an internal feature that points to a platform, we can use that + let internal_feature = self.package().features.get(feature); + if let Some(internal_feature) = internal_feature { + for feature in internal_feature { + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + let auto = Platform::autodetect_from_cargo_feature(dx_feature); + if auto.is_some() { + return auto; + } + } + } + } + } + + None + } + + /// Gather the features that are enabled for the package + pub(crate) fn platformless_features(&self) -> Vec { + let default = self.package().features.get("default").unwrap(); + let mut kept_features = vec![]; + + // Only keep the top-level features in the default list that don't point to a platform directly + // IE we want to drop `web` if default = ["web"] + 'top: for feature in default { + // Don't keep features that point to a platform via dioxus/blah + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { + continue 'top; + } + } + + // Don't keep features that point to a platform via an internal feature + if let Some(internal_feature) = self.package().features.get(feature) { + for feature in internal_feature { + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { + continue 'top; + } + } + } + } + + // Otherwise we can keep it + kept_features.push(feature.to_string()); + } + + kept_features + } + + /// Return the list of paths that we should watch for changes. + pub(crate) fn watch_paths(&self) -> Vec { + let mut watched_paths = vec![]; + + // Get a list of *all* the crates with Rust code that we need to watch. + // This will end up being dependencies in the workspace and non-workspace dependencies on the user's computer. + let mut watched_crates = self.local_dependencies(); + watched_crates.push(self.crate_dir()); + + // Now, watch all the folders in the crates, but respecting their respective ignore files + for krate_root in watched_crates { + // Build the ignore builder for this crate, but with our default ignore list as well + let ignore = self.ignore_for_krate(&krate_root); + + for entry in krate_root.read_dir().unwrap() { + let Ok(entry) = entry else { + continue; + }; + + if ignore + .matched(entry.path(), entry.path().is_dir()) + .is_ignore() + { + continue; + } + + watched_paths.push(entry.path().to_path_buf()); + } + } + + watched_paths.dedup(); + + watched_paths + } + + fn ignore_for_krate(&self, path: &Path) -> ignore::gitignore::Gitignore { + let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(path); + for path in self.default_ignore_list() { + ignore_builder + .add_line(None, path) + .expect("failed to add path to file excluder"); + } + ignore_builder.build().unwrap() + } + + /// Get all the Manifest paths for dependencies that we should watch. Will not return anything + /// in the `.cargo` folder - only local dependencies will be watched. + /// + /// This returns a list of manifest paths + /// + /// Extend the watch path to include: + /// + /// - the assets directory - this is so we can hotreload CSS and other assets by default + /// - the Cargo.toml file - this is so we can hotreload the project if the user changes dependencies + /// - the Dioxus.toml file - this is so we can hotreload the project if the user changes the Dioxus config + pub(crate) fn local_dependencies(&self) -> Vec { + let mut paths = vec![]; + + for (dependency, _edge) in self.krates.get_deps(self.package) { + let krate = match dependency { + krates::Node::Krate { krate, .. } => krate, + krates::Node::Feature { krate_index, .. } => &self.krates[krate_index.index()], + }; + + if krate + .manifest_path + .components() + .any(|c| c.as_str() == ".cargo") + { + continue; + } + + paths.push( + krate + .manifest_path + .parent() + .unwrap() + .to_path_buf() + .into_std_path_buf(), + ); + } + + paths + } + + pub(crate) fn all_watched_crates(&self) -> Vec { + let mut krates: Vec = self + .local_dependencies() + .into_iter() + .map(|p| { + p.parent() + .expect("Local manifest to exist and have a parent") + .to_path_buf() + }) + .chain(Some(self.crate_dir())) + .collect(); + + krates.dedup(); + + krates + } +} + +impl std::fmt::Debug for DioxusCrate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DioxusCrate") + .field("package", &self.krates[self.package]) + .field("dioxus_config", &self.config) + .field("target", &self.target) + .finish() } } // Find the main package in the workspace -fn find_main_package(package: Option, krates: &Krates) -> Result { +fn find_main_package(krates: &Krates, package: Option) -> Result { let kid = match package { Some(package) => { let mut workspace_members = krates.workspace_members(); @@ -103,7 +584,7 @@ fn find_main_package(package: Option, krates: &Krates) -> Result { // Otherwise find the package that is the closest parent of the current directory @@ -131,235 +612,10 @@ fn find_main_package(package: Option, krates: &Krates) -> Result, - pub package: NodeId, - pub dioxus_config: DioxusConfig, - pub target: Target, -} - -impl DioxusCrate { - pub fn new(target: &TargetArgs) -> Result { - let mut cmd = Cmd::new(); - cmd.features(target.features.clone()); - let builder = krates::Builder::new(); - let krates = builder.build(cmd, |_| {})?; - - let package = find_main_package(target.package.clone(), &krates)?; - - let dioxus_config = load_dioxus_config(&krates, package)?.unwrap_or_default(); - - let package_name = krates[package].name.clone(); - let target_kind = if target.example.is_some() { - TargetKind::Example - } else { - TargetKind::Bin - }; - let target_name = target - .example - .clone() - .or(target.bin.clone()) - .unwrap_or(package_name); - let main_package = &krates[package]; - let target = main_package - .targets - .iter() - .find(|target| { - target_name == target.name.as_str() && target.kind.contains(&target_kind) - }) - .ok_or(CrateConfigError::TargetNotFound(target_name))? - .clone(); - - Ok(Self { - krates: Arc::new(krates), - package, - dioxus_config, - target, - }) - } - - /// Compose an asset directory. Represents the typical "public" directory - /// with publicly available resources (configurable in the `Dioxus.toml`). - pub fn asset_dir(&self) -> PathBuf { - self.crate_dir() - .join(&self.dioxus_config.application.asset_dir) - } - - /// Compose an out directory. Represents the typical "dist" directory that - /// is "distributed" after building an application (configurable in the - /// `Dioxus.toml`). - pub fn out_dir(&self) -> PathBuf { - self.workspace_dir() - .join(&self.dioxus_config.application.out_dir) - } - - /// Get the workspace directory for the crate - pub fn workspace_dir(&self) -> PathBuf { - self.krates.workspace_root().as_std_path().to_path_buf() - } - - /// Get the directory of the crate - pub fn crate_dir(&self) -> PathBuf { - self.package() - .manifest_path - .parent() - .unwrap() - .as_std_path() - .to_path_buf() - } - - /// Get the main source file of the target - pub fn main_source_file(&self) -> PathBuf { - self.target.src_path.as_std_path().to_path_buf() - } - - /// Get the package we are currently in - pub fn package(&self) -> &krates::cm::Package { - &self.krates[self.package] - } - - /// Get the name of the package we are compiling - pub fn executable_name(&self) -> &str { - &self.target.name - } - - /// Get the type of executable we are compiling - pub fn executable_type(&self) -> krates::cm::TargetKind { - self.target.kind[0].clone() - } - - pub fn features_for_platform(&mut self, platform: Platform) -> Vec { - let package = self.package(); - // Try to find the feature that activates the dioxus feature for the given platform - let dioxus_feature = platform.feature_name(); - let feature = package.features.iter().find_map(|(key, features)| { - // Find a feature that starts with dioxus/ or dioxus?/ - for feature in features { - if let Some((_, after_dioxus)) = feature.split_once("dioxus") { - if let Some(dioxus_feature_enabled) = - after_dioxus.trim_start_matches('?').strip_prefix('/') - { - // If that enables the feature we are looking for, return that feature - if dioxus_feature_enabled == dioxus_feature { - return Some(key.clone()); - } - } - } - } - None - }); - - feature.into_iter().collect() - } - - /// Check if assets should be pre_compressed. This will only be true in release mode if the user has enabled pre_compress in the web config. - pub fn should_pre_compress_web_assets(&self, release: bool) -> bool { - self.dioxus_config.web.pre_compress && release - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Executable { - pub name: String, - pub ty: ExecutableType, -} - -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] -pub enum ExecutableType { - Binary, - Lib, - Example, -} - -impl ExecutableType { - /// Get the name of the executable if it is a binary or an example. - pub fn executable(&self) -> bool { - matches!(self, Self::Binary | Self::Example) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LoadDioxusConfigError { - location: String, - error: String, -} - -impl std::fmt::Display for LoadDioxusConfigError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {}", self.location, self.error) - } -} - -impl std::error::Error for LoadDioxusConfigError {} - -#[derive(Debug)] -#[non_exhaustive] -pub enum CrateConfigError { - Cargo(CargoError), - Io(std::io::Error), - Toml(toml::de::Error), - LoadDioxusConfig(LoadDioxusConfigError), - TargetNotFound(String), - Krates(krates::Error), - PackageNotFound(String), - CurrentPackageNotFound, -} - -impl From for CrateConfigError { - fn from(err: CargoError) -> Self { - Self::Cargo(err) - } -} - -impl From for CrateConfigError { - fn from(err: std::io::Error) -> Self { - Self::Io(err) - } -} - -impl From for CrateConfigError { - fn from(err: toml::de::Error) -> Self { - Self::Toml(err) - } -} - -impl From for CrateConfigError { - fn from(err: LoadDioxusConfigError) -> Self { - Self::LoadDioxusConfig(err) - } -} - -impl From for CrateConfigError { - fn from(err: krates::Error) -> Self { - Self::Krates(err) - } -} - -impl Display for CrateConfigError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Cargo(err) => write!(f, "{}", err), - Self::Io(err) => write!(f, "{}", err), - Self::Toml(err) => write!(f, "{}", err), - Self::LoadDioxusConfig(err) => write!(f, "{}", err), - Self::TargetNotFound(target) => { - write!(f, "Failed to find target with name: {}", target) - } - Self::Krates(err) => write!(f, "{}", err), - Self::PackageNotFound(package) => write!(f, "Package not found: {}", package), - Self::CurrentPackageNotFound => write!(f, "Failed to find current package"), - } - } -} - -impl std::error::Error for CrateConfigError {} diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 6d37636ed..034ea6dbd 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -1,11 +1,10 @@ +use crate::metadata::CargoError; use thiserror::Error as ThisError; -use crate::{metadata::CargoError, CrateConfigError, LoadDioxusConfigError}; - -pub type Result = std::result::Result; +pub(crate) type Result = std::result::Result; #[derive(ThisError, Debug)] -pub enum Error { +pub(crate) enum Error { /// Used when errors need to propagate but are too unique to be typed #[error("{0}")] Unique(String), @@ -14,37 +13,22 @@ pub enum Error { IO(#[from] std::io::Error), #[error("Format Error: {0}")] - FormatError(#[from] std::fmt::Error), + Format(#[from] std::fmt::Error), #[error("Format failed: {0}")] - ParseError(String), + Parse(String), #[error("Runtime Error: {0}")] - RuntimeError(String), - - #[error("Failed to write error")] - FailedToWrite, - - #[error("Build Failed: {0}")] - BuildFailed(String), + Runtime(String), #[error("Cargo Error: {0}")] - CargoError(String), - - #[error("Couldn't retrieve cargo metadata")] - CargoMetadata(#[source] cargo_metadata::Error), - - #[error("{0}")] - CustomError(String), + Cargo(#[from] CargoError), #[error("Invalid proxy URL: {0}")] InvalidProxy(#[from] hyper::http::uri::InvalidUri), #[error("Failed to establish proxy: {0}")] - ProxySetupError(String), - - #[error("Error proxying request: {0}")] - ProxyRequestError(hyper::Error), + ProxySetup(String), #[error(transparent)] Other(#[from] anyhow::Error), @@ -64,43 +48,12 @@ impl From for Error { impl From for Error { fn from(e: html_parser::Error) -> Self { - Self::ParseError(e.to_string()) + Self::Parse(e.to_string()) } } impl From for Error { fn from(e: hyper::Error) -> Self { - Self::RuntimeError(e.to_string()) + Self::Runtime(e.to_string()) } } - -impl From for Error { - fn from(e: LoadDioxusConfigError) -> Self { - Self::RuntimeError(e.to_string()) - } -} - -impl From for Error { - fn from(e: CargoError) -> Self { - Self::CargoError(e.to_string()) - } -} - -impl From for Error { - fn from(e: CrateConfigError) -> Self { - Self::RuntimeError(e.to_string()) - } -} - -#[macro_export] -macro_rules! custom_error { - ($msg:literal $(,)?) => { - Err(Error::CustomError(format!($msg))) - }; - ($err:expr $(,)?) => { - Err(Error::from($err)) - }; - ($fmt:expr, $($arg:tt)*) => { - Err(Error::CustomError(format!($fmt, $($arg)*))) - }; -} diff --git a/packages/cli/src/fastfs.rs b/packages/cli/src/fastfs.rs new file mode 100644 index 000000000..106a513dd --- /dev/null +++ b/packages/cli/src/fastfs.rs @@ -0,0 +1,126 @@ +//! Methods for working with the filesystem that are faster than the std fs methods +//! Uses stuff like rayon, caching, and other optimizations +//! +//! Allows configuration in case you want to do some work while copying and allows you to track progress + +use std::{ + ffi::OsString, + path::{Path, PathBuf}, +}; + +use brotli::enc::BrotliEncoderParams; +use walkdir::WalkDir; + +pub fn copy_asset(src: &Path, dest: &Path) -> std::io::Result<()> { + if src.is_dir() { + copy_dir_to(src, dest, false)?; + } else { + std::fs::copy(src, dest)?; + } + + Ok(()) +} + +pub(crate) fn copy_dir_to( + src_dir: &Path, + dest_dir: &Path, + pre_compress: bool, +) -> std::io::Result<()> { + let entries = std::fs::read_dir(src_dir)?; + let mut children: Vec>> = Vec::new(); + + for entry in entries.flatten() { + let entry_path = entry.path(); + let path_relative_to_src = entry_path.strip_prefix(src_dir).unwrap(); + let output_file_location = dest_dir.join(path_relative_to_src); + children.push(std::thread::spawn(move || { + if entry.file_type()?.is_dir() { + // If the file is a directory, recursively copy it into the output directory + if let Err(err) = copy_dir_to(&entry_path, &output_file_location, pre_compress) { + tracing::error!( + "Failed to pre-compress directory {}: {}", + entry_path.display(), + err + ); + } + } else { + // Make sure the directory exists + std::fs::create_dir_all(output_file_location.parent().unwrap())?; + // Copy the file to the output directory + std::fs::copy(&entry_path, &output_file_location)?; + + // Then pre-compress the file if needed + if pre_compress { + if let Err(err) = pre_compress_file(&output_file_location) { + tracing::error!( + "Failed to pre-compress static assets {}: {}", + output_file_location.display(), + err + ); + } + // If pre-compression isn't enabled, we should remove the old compressed file if it exists + } else if let Some(compressed_path) = compressed_path(&output_file_location) { + _ = std::fs::remove_file(compressed_path); + } + } + + Ok(()) + })); + } + for child in children { + child.join().unwrap()?; + } + Ok(()) +} + +/// Get the path to the compressed version of a file +fn compressed_path(path: &Path) -> Option { + let new_extension = match path.extension() { + Some(ext) => { + if ext.to_string_lossy().to_lowercase().ends_with("br") { + return None; + } + let mut ext = ext.to_os_string(); + ext.push(".br"); + ext + } + None => OsString::from("br"), + }; + + Some(path.with_extension(new_extension)) +} + +/// pre-compress a file with brotli +pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> { + let Some(compressed_path) = compressed_path(path) else { + return Ok(()); + }; + + let file = std::fs::File::open(path)?; + let mut stream = std::io::BufReader::new(file); + let mut buffer = std::fs::File::create(compressed_path)?; + let params = BrotliEncoderParams::default(); + brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?; + + Ok(()) +} + +/// pre-compress all files in a folder +pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::Result<()> { + let walk_dir = WalkDir::new(path); + for entry in walk_dir.into_iter().filter_map(|e| e.ok()) { + let entry_path = entry.path(); + if entry_path.is_file() { + if pre_compress { + if let Err(err) = pre_compress_file(entry_path) { + tracing::error!("Failed to pre-compress file {entry_path:?}: {err}"); + } + } + // If pre-compression isn't enabled, we should remove the old compressed file if it exists + else if let Some(compressed_path) = compressed_path(entry_path) { + _ = std::fs::remove_file(compressed_path); + } + } + } + Ok(()) +} diff --git a/packages/cli/src/filemap.rs b/packages/cli/src/filemap.rs new file mode 100644 index 000000000..33c3f2e42 --- /dev/null +++ b/packages/cli/src/filemap.rs @@ -0,0 +1,168 @@ +use dioxus_core::internal::{ + HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey, +}; +use dioxus_core_types::HotReloadingContext; +use dioxus_rsx::CallBody; +use dioxus_rsx_hotreload::{ChangedRsx, HotReloadResult}; +use std::path::PathBuf; +use std::{collections::HashMap, path::Path}; +use syn::spanned::Spanned; + +/// A struct that stores state of rsx! files and their parsed bodies. +/// +/// This keeps track of changes to rsx files and helps determine if a file can be hotreloaded or if +/// the project needs to be rebuilt. +pub(crate) struct HotreloadFilemap { + /// Map of rust files to their contents + /// + /// Once this is created, we won't change the contents, to preserve the ability to hotreload + /// from the original source mapping, unless the file change results in a full rebuild. + map: HashMap, +} + +struct CachedFile { + contents: String, + most_recent: Option, + templates: HashMap, +} + +pub enum HotreloadResult { + Rsx(Vec), + Notreloadable, + NotParseable, +} + +impl HotreloadFilemap { + /// Create a new empty filemap. + /// + /// Make sure to fill the filemap, either automatically with `fill_from_filesystem` or manually with `add_file`; + pub fn new() -> Self { + Self { + map: Default::default(), + } + } + + /// Add a file to the filemap. + pub(crate) fn add_file(&mut self, path: PathBuf, contents: String) { + self.map.insert( + path, + CachedFile { + contents, + most_recent: None, + templates: Default::default(), + }, + ); + } + + /// Commit the changes to the filemap, overwriting the contents of the files + /// + /// Removes any cached templates and replaces the contents of the files with the most recent + /// + /// todo: we should-reparse the contents so we never send a new version, ever + pub fn force_rebuild(&mut self) { + for cached_file in self.map.values_mut() { + if let Some(most_recent) = cached_file.most_recent.take() { + cached_file.contents = most_recent; + } + cached_file.templates.clear(); + } + } + + /// Try to update the rsx in a file, returning the templates that were hotreloaded + /// + /// If the templates could not be hotreloaded, this will return an error. This error isn't fatal, per se, + /// but it does mean that we could not successfully hotreload the file in-place. + /// + /// It's expected that the file path you pass in is relative the crate root. We have no way of + /// knowing if it's *not*, so we'll assume it is. + /// + /// This does not do any caching on what intermediate state, like previous hotreloads, so you need + /// to do that yourself. + pub(crate) fn update_rsx( + &mut self, + path: &Path, + new_contents: String, + ) -> HotreloadResult { + // Get the cached file if it exists + let Some(cached_file) = self.map.get_mut(path) else { + return HotreloadResult::NotParseable; + }; + + // We assume we can parse the old file and the new file + // We should just ignore hotreloading files that we can't parse + // todo(jon): we could probably keep the old `File` around instead of re-parsing on every hotreload + let (Ok(old_file), Ok(new_file)) = ( + syn::parse_file(&cached_file.contents), + syn::parse_file(&new_contents), + ) else { + tracing::debug!("Diff rsx returned not parseable"); + return HotreloadResult::NotParseable; + }; + + // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version + cached_file.most_recent = Some(new_contents); + + // todo(jon): allow server-fn hotreloading + // also whyyyyyyyyy is this (new, old) instead of (old, new)? smh smh smh + let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else { + tracing::debug!("Diff rsx returned notreladable"); + return HotreloadResult::Notreloadable; + }; + + let mut out_templates = vec![]; + for ChangedRsx { old, new } in changed_rsx { + let old_start = old.span().start(); + + let old_parsed = syn::parse2::(old.tokens); + let new_parsed = syn::parse2::(new.tokens); + let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else { + continue; + }; + + // Format the template location, normalizing the path + let file_name: String = path + .components() + .map(|c| c.as_os_str().to_string_lossy()) + .collect::>() + .join("/"); + + // Returns a list of templates that are hotreloadable + let results = HotReloadResult::new::( + &old_call_body.body, + &new_call_body.body, + file_name.clone(), + ); + + // If no result is returned, we can't hotreload this file and need to keep the old file + let Some(results) = results else { + return HotreloadResult::Notreloadable; + }; + + // Only send down templates that have roots, and ideally ones that have changed + // todo(jon): maybe cache these and don't send them down if they're the same + for (index, template) in results.templates { + if template.roots.is_empty() { + continue; + } + + // Create the key we're going to use to identify this template + let key = TemplateGlobalKey { + file: file_name.clone(), + line: old_start.line, + column: old_start.column + 1, + index, + }; + + // if the template is the same, don't send its + if cached_file.templates.get(&key) == Some(&template) { + continue; + }; + + cached_file.templates.insert(key.clone(), template.clone()); + out_templates.push(HotReloadTemplateWithLocation { template, key }); + } + } + + HotreloadResult::Rsx(out_templates) + } +} diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 5ea1332d3..5f34b12eb 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -1,88 +1,77 @@ #![doc = include_str!("../README.md")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] +#![cfg_attr(docsrs, feature(doc_cfg))] -pub mod assets; -pub mod builder; -pub mod cli; -pub mod config; -pub mod dioxus_crate; -pub mod dx_build_info; -pub mod error; -pub mod metadata; -pub mod serve; -pub mod settings; -pub mod tracer; +mod assets; +mod builder; +mod bundle_utils; +mod cli; +mod config; +mod dioxus_crate; +mod dx_build_info; +mod error; +mod fastfs; +mod filemap; +mod metadata; +mod platform; +mod profiles; +mod rustup; +mod serve; +mod settings; +mod tooling; +mod tracer; +pub(crate) use builder::*; pub(crate) use cli::*; +pub(crate) use config::*; pub(crate) use dioxus_crate::*; pub(crate) use error::*; +pub(crate) use filemap::*; +pub(crate) use platform::*; +pub(crate) use rustup::*; pub(crate) use settings::*; -pub(crate) use tracer::{TraceMsg, TraceSrc}; +pub(crate) use tracer::*; use anyhow::Context; use clap::Parser; - use Commands::*; #[tokio::main] async fn main() -> anyhow::Result<()> { - let args = Cli::parse(); + // If we're being ran as a linker (likely from ourselves), we want to act as a linker instead. + if let Some(link_action) = link::LinkAction::from_env() { + return link_action.run(); + } - let log_control = tracer::build_tracing(); + // Start the tracer so it captures logs from the build engine before we start the builder + TraceController::initialize(); - match args.action { + match Cli::parse().action { Translate(opts) => opts .translate() - .context(error_wrapper("Translation of HTML into RSX failed")), + .context("⛔️ Translation of HTML into RSX failed:"), - New(opts) => opts - .create() - .context(error_wrapper("Creating new project failed")), + New(opts) => opts.create().context("🚫 Creating new project failed:"), - Init(opts) => opts - .init() - .context(error_wrapper("Initializing a new project failed")), + Init(opts) => opts.init().context("🚫 Initializing a new project failed:"), - Config(opts) => opts - .config() - .context(error_wrapper("Configuring new project failed")), + Config(opts) => opts.config().context("🚫 Configuring new project failed:"), - Autoformat(opts) => opts - .autoformat() - .context(error_wrapper("Error autoformatting RSX")), + Autoformat(opts) => opts.autoformat().context("🚫 Error autoformatting RSX:"), - Check(opts) => opts - .check() - .await - .context(error_wrapper("Error checking RSX")), + Check(opts) => opts.check().await.context("🚫 Error checking RSX:"), - Link(opts) => opts - .link() - .context(error_wrapper("Error with linker passthrough")), + Clean(opts) => opts.clean().context("🚫 Cleaning project failed:"), - Build(mut opts) => opts - .run() - .await - .context(error_wrapper("Building project failed")), + Build(mut opts) => opts.build_it().await.context("🚫 Building project failed:"), - Clean(opts) => opts - .clean() - .context(error_wrapper("Cleaning project failed")), + Serve(opts) => opts.serve().await.context("🚫 Serving project failed:"), - Serve(opts) => opts - .serve(log_control) - .await - .context(error_wrapper("Serving project failed")), + Bundle(opts) => opts.bundle().await.context("🚫 Bundling project failed:"), - Bundle(opts) => opts - .bundle() - .await - .context(error_wrapper("Bundling project failed")), + Run(opts) => opts.run().await.context("🚫 Running project failed:"), + + Doctor(opts) => opts.run().await.context("🚫 Checking project failed:"), } } - -/// Simplifies error messages that use the same pattern. -fn error_wrapper(message: &str) -> String { - format!("🚫 {message}:") -} diff --git a/packages/cli/src/metadata.rs b/packages/cli/src/metadata.rs index b9acbdf75..f9e072683 100644 --- a/packages/cli/src/metadata.rs +++ b/packages/cli/src/metadata.rs @@ -8,12 +8,12 @@ use std::{ }; #[derive(Debug, Clone)] -pub struct CargoError { +pub(crate) struct CargoError { msg: String, } impl CargoError { - pub fn new(msg: String) -> Self { + pub(crate) fn new(msg: String) -> Self { Self { msg } } } diff --git a/packages/cli/src/platform.rs b/packages/cli/src/platform.rs new file mode 100644 index 000000000..0720290d1 --- /dev/null +++ b/packages/cli/src/platform.rs @@ -0,0 +1,186 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; + +#[derive( + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Debug, + Default, + clap::ValueEnum, +)] +#[non_exhaustive] +pub(crate) enum Platform { + /// Targeting the web platform using WASM + #[clap(name = "web")] + #[serde(rename = "web")] + #[default] + Web, + + /// Targeting macos desktop + /// When running on macos, you can also use `--platform desktop` to build for the desktop + #[cfg_attr(target_os = "macos", clap(alias = "desktop"))] + #[clap(name = "macos")] + #[serde(rename = "macos")] + MacOS, + + /// Targeting windows desktop + /// When running on windows, you can also use `--platform desktop` to build for the desktop + #[cfg_attr(target_os = "windows", clap(alias = "desktop"))] + #[clap(name = "windows")] + #[serde(rename = "windows")] + Windows, + + /// Targeting linux desktop + /// When running on linux, you can also use `--platform desktop` to build for the desktop + #[cfg_attr(target_os = "linux", clap(alias = "desktop"))] + #[clap(name = "linux")] + #[serde(rename = "linux")] + Linux, + + /// Targeting the ios platform + /// + /// Can't work properly if you're not building from an Apple device. + #[clap(name = "ios")] + #[serde(rename = "ios")] + Ios, + + /// Targeting the android platform + #[clap(name = "android")] + #[serde(rename = "android")] + Android, + + /// Targeting the server platform using Axum and Dioxus-Fullstack + /// + /// This is implicitly passed if `fullstack` is enabled as a feature. Using this variant simply + /// means you're only building the server variant without the `.wasm` to serve. + #[clap(name = "server")] + #[serde(rename = "server")] + Server, + + /// Targeting the static generation platform using SSR and Dioxus-Fullstack + #[clap(name = "liveview")] + #[serde(rename = "liveview")] + Liveview, +} + +/// An error that occurs when a platform is not recognized +pub(crate) struct UnknownPlatformError; + +impl std::fmt::Display for UnknownPlatformError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Unknown platform") + } +} + +impl FromStr for Platform { + type Err = UnknownPlatformError; + + fn from_str(s: &str) -> Result { + match s { + "web" => Ok(Self::Web), + "macos" => Ok(Self::MacOS), + "windows" => Ok(Self::Windows), + "linux" => Ok(Self::Linux), + "liveview" => Ok(Self::Liveview), + "server" => Ok(Self::Server), + "ios" => Ok(Self::Ios), + "android" => Ok(Self::Android), + _ => Err(UnknownPlatformError), + } + } +} + +impl Display for Platform { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Platform::Web => "web", + Platform::MacOS => "macos", + Platform::Windows => "windows", + Platform::Linux => "linux", + Platform::Ios => "ios", + Platform::Android => "android", + Platform::Server => "server", + Platform::Liveview => "liveview", + }) + } +} + +impl Platform { + /// Get the feature name for the platform in the dioxus crate + pub(crate) fn feature_name(&self) -> &str { + match self { + Platform::Web => "web", + Platform::MacOS => "desktop", + Platform::Windows => "desktop", + Platform::Linux => "desktop", + Platform::Server => "server", + Platform::Liveview => "liveview", + Platform::Ios => "mobile", + Platform::Android => "mobile", + } + } + + /// Get the name of the folder we need to generate for this platform + /// + /// Note that web and server share the same platform folder since we'll export the web folder as a bundle on its own + pub(crate) fn build_folder_name(&self) -> &'static str { + match self { + Platform::Web => "web", + Platform::Server => "web", + Platform::Liveview => "liveview", + Platform::Ios => "ios", + Platform::Android => "android", + Platform::Windows => "windows", + Platform::Linux => "linux", + Platform::MacOS => "macos", + } + } + + pub(crate) fn expected_name(&self) -> &'static str { + match self { + Platform::Web => "Web", + Platform::MacOS => "Desktop MacOS", + Platform::Windows => "Desktop Windows", + Platform::Linux => "Desktop Linux", + Platform::Ios => "Mobile iOS", + Platform::Android => "Mobile Android", + Platform::Server => "Server", + Platform::Liveview => "Liveview", + } + } + + pub(crate) fn autodetect_from_cargo_feature(feature: &str) -> Option { + match feature { + "web" => Some(Platform::Web), + "desktop" => { + #[cfg(target_os = "macos")] + { + Some(Platform::MacOS) + } + #[cfg(target_os = "windows")] + { + Some(Platform::Windows) + } + #[cfg(target_os = "linux")] + { + Some(Platform::Linux) + } + } + "mobile" => { + tracing::warn!("Could not autodetect mobile platform. Mobile platforms must be explicitly specified. Pass `--platform ios` or `--platform android` instead."); + None + } + "liveview" => Some(Platform::Liveview), + "server" => Some(Platform::Server), + _ => None, + } + } +} diff --git a/packages/cli/src/serve/logs_tab.rs b/packages/cli/src/profiles.rs similarity index 100% rename from packages/cli/src/serve/logs_tab.rs rename to packages/cli/src/profiles.rs diff --git a/packages/cli/src/rustup.rs b/packages/cli/src/rustup.rs new file mode 100644 index 000000000..380a20da9 --- /dev/null +++ b/packages/cli/src/rustup.rs @@ -0,0 +1,170 @@ +use crate::Result; +use anyhow::Context; +use std::path::PathBuf; +use tokio::process::Command; + +#[derive(Debug, Default)] +pub struct RustupShow { + pub default_host: String, + pub rustup_home: PathBuf, + pub installed_toolchains: Vec, + pub installed_targets: Vec, + pub active_rustc: String, + pub active_toolchain: String, +} +impl RustupShow { + /// Collect the output of `rustup show` and parse it + pub async fn from_cli() -> Result { + let output = Command::new("rustup").args(["show"]).output().await?; + let stdout = + String::from_utf8(output.stdout).context("Failed to parse rustup show output")?; + + Ok(RustupShow::from_stdout(stdout)) + } + + /// Parse the output of `rustup show` + pub fn from_stdout(output: String) -> RustupShow { + // I apologize for this hand-rolled parser + + let mut result = RustupShow::default(); + let mut current_section = ""; + + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + if line.starts_with("Default host: ") { + result.default_host = line.strip_prefix("Default host: ").unwrap().to_string(); + } else if line.starts_with("rustup home: ") { + result.rustup_home = + PathBuf::from(line.strip_prefix("rustup home: ").unwrap().trim()); + } else if line == "installed toolchains" { + current_section = "toolchains"; + } else if line == "installed targets for active toolchain" { + current_section = "targets"; + } else if line == "active toolchain" { + current_section = "active_toolchain"; + } else { + if line.starts_with("---") || line.is_empty() { + continue; + } + match current_section { + "toolchains" => result + .installed_toolchains + .push(line.trim_end_matches(" (default)").to_string()), + "targets" => result.installed_targets.push(line.to_string()), + "active_toolchain" => { + if result.active_toolchain.is_empty() { + result.active_toolchain = line.to_string(); + } else if line.starts_with("rustc ") { + result.active_rustc = line.to_string(); + } + } + _ => {} + } + } + } + + result + } + + pub fn has_wasm32_unknown_unknown(&self) -> bool { + self.installed_targets + .contains(&"wasm32-unknown-unknown".to_string()) + } +} + +#[test] +fn parses_rustup_show() { + let output = r#" +Default host: aarch64-apple-darwin +rustup home: /Users/jonkelley/.rustup + +installed toolchains +-------------------- + +stable-aarch64-apple-darwin (default) +nightly-2021-07-06-aarch64-apple-darwin +nightly-2021-09-24-aarch64-apple-darwin +nightly-2022-03-10-aarch64-apple-darwin +nightly-2023-03-18-aarch64-apple-darwin +nightly-2024-01-11-aarch64-apple-darwin +nightly-aarch64-apple-darwin +1.58.1-aarch64-apple-darwin +1.60.0-aarch64-apple-darwin +1.68.2-aarch64-apple-darwin +1.69.0-aarch64-apple-darwin +1.71.1-aarch64-apple-darwin +1.72.1-aarch64-apple-darwin +1.73.0-aarch64-apple-darwin +1.74.1-aarch64-apple-darwin +1.77.2-aarch64-apple-darwin +1.78.0-aarch64-apple-darwin +1.79.0-aarch64-apple-darwin +1.49-aarch64-apple-darwin +1.55-aarch64-apple-darwin +1.56-aarch64-apple-darwin +1.57-aarch64-apple-darwin +1.66-aarch64-apple-darwin +1.69-aarch64-apple-darwin +1.70-aarch64-apple-darwin +1.74-aarch64-apple-darwin + +installed targets for active toolchain +-------------------------------------- + +aarch64-apple-darwin +aarch64-apple-ios +aarch64-apple-ios-sim +aarch64-linux-android +aarch64-unknown-linux-gnu +armv7-linux-androideabi +i686-linux-android +thumbv6m-none-eabi +thumbv7em-none-eabihf +wasm32-unknown-unknown +x86_64-apple-darwin +x86_64-apple-ios +x86_64-linux-android +x86_64-pc-windows-msvc +x86_64-unknown-linux-gnu + +active toolchain +---------------- + +stable-aarch64-apple-darwin (default) +rustc 1.79.0 (129f3b996 2024-06-10) +"#; + let show = RustupShow::from_stdout(output.to_string()); + assert_eq!(show.default_host, "aarch64-apple-darwin"); + assert_eq!(show.rustup_home, PathBuf::from("/Users/jonkelley/.rustup")); + assert_eq!( + show.active_toolchain, + "stable-aarch64-apple-darwin (default)" + ); + assert_eq!(show.active_rustc, "rustc 1.79.0 (129f3b996 2024-06-10)"); + assert_eq!(show.installed_toolchains.len(), 26); + assert_eq!(show.installed_targets.len(), 15); + assert_eq!( + show.installed_targets, + vec![ + "aarch64-apple-darwin".to_string(), + "aarch64-apple-ios".to_string(), + "aarch64-apple-ios-sim".to_string(), + "aarch64-linux-android".to_string(), + "aarch64-unknown-linux-gnu".to_string(), + "armv7-linux-androideabi".to_string(), + "i686-linux-android".to_string(), + "thumbv6m-none-eabi".to_string(), + "thumbv7em-none-eabihf".to_string(), + "wasm32-unknown-unknown".to_string(), + "x86_64-apple-darwin".to_string(), + "x86_64-apple-ios".to_string(), + "x86_64-linux-android".to_string(), + "x86_64-pc-windows-msvc".to_string(), + "x86_64-unknown-linux-gnu".to_string(), + ] + ) +} diff --git a/packages/cli/src/serve/ansi_buffer.rs b/packages/cli/src/serve/ansi_buffer.rs new file mode 100644 index 000000000..c1e070f80 --- /dev/null +++ b/packages/cli/src/serve/ansi_buffer.rs @@ -0,0 +1,172 @@ +use ratatui::prelude::*; +use std::fmt::{self, Display, Formatter}; + +/// A buffer that can be rendered to and then dumped as raw ansi codes +/// +/// This is taken from a PR on the ratatui repo (https://github.com/ratatui/ratatui/pull/1065) and +/// modified to be more appropriate for our use case. +pub struct AnsiStringBuffer { + buf: Buffer, +} + +// The sentinel character used to mark the end of the ansi string so when we dump it, we know where to stop +// Not sure if we actually still need this.... +const SENTINEL: &str = "✆"; + +impl AnsiStringBuffer { + /// Creates a new `AnsiStringBuffer` with the given width and height. + pub(crate) fn new(width: u16, height: u16) -> Self { + Self { + buf: Buffer::empty(Rect::new(0, 0, width, height)), + } + } + + /// Renders the given widget to the buffer, returning the string with the ansi codes + pub(crate) fn render(mut self, widget: impl Widget) -> String { + widget.render(self.buf.area, &mut self.buf); + self.trim_end(); + self.to_string() + } + + /// Trims the buffer to the last line, returning the number of cells to be rendered + #[allow(deprecated)] + fn trim_end(&mut self) { + for y in 0..self.buf.area.height { + let start_x = self.buf.area.width; + let mut first_non_empty = start_x - 1; + for x in (0..start_x).rev() { + if self.buf.get(x, y) != &buffer::Cell::EMPTY { + break; + } + first_non_empty = x; + } + self.buf.get_mut(first_non_empty, y).set_symbol(SENTINEL); + } + } + + fn write_fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut last_style = None; + for y in 0..self.buf.area.height { + for x in 0..self.buf.area.width { + let cell = self.buf.cell((x, y)).unwrap(); + if cell.symbol() == SENTINEL { + f.write_str("\n")?; + break; + } + + let style = (cell.fg, cell.bg, cell.modifier); + if last_style.is_none() || last_style != Some(style) { + write_cell_style(f, cell)?; + last_style = Some(style); + } + f.write_str(cell.symbol())?; + } + } + f.write_str("\u{1b}[0m") + } +} + +impl Display for AnsiStringBuffer { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.write_fmt(f) + } +} + +fn write_cell_style(f: &mut Formatter, cell: &buffer::Cell) -> fmt::Result { + f.write_str("\u{1b}[")?; + write_modifier(f, cell.modifier)?; + write_fg(f, cell.fg)?; + write_bg(f, cell.bg)?; + f.write_str("m") +} + +fn write_modifier(f: &mut Formatter, modifier: Modifier) -> fmt::Result { + if modifier.contains(Modifier::BOLD) { + f.write_str("1;")?; + } + if modifier.contains(Modifier::DIM) { + f.write_str("2;")?; + } + if modifier.contains(Modifier::ITALIC) { + f.write_str("3;")?; + } + if modifier.contains(Modifier::UNDERLINED) { + f.write_str("4;")?; + } + if modifier.contains(Modifier::SLOW_BLINK) { + f.write_str("5;")?; + } + if modifier.contains(Modifier::RAPID_BLINK) { + f.write_str("6;")?; + } + if modifier.contains(Modifier::REVERSED) { + f.write_str("7;")?; + } + if modifier.contains(Modifier::HIDDEN) { + f.write_str("8;")?; + } + if modifier.contains(Modifier::CROSSED_OUT) { + f.write_str("9;")?; + } + Ok(()) +} + +fn write_fg(f: &mut Formatter, color: Color) -> fmt::Result { + f.write_str(match color { + Color::Reset => "39", + Color::Black => "30", + Color::Red => "31", + Color::Green => "32", + Color::Yellow => "33", + Color::Blue => "34", + Color::Magenta => "35", + Color::Cyan => "36", + Color::Gray => "37", + Color::DarkGray => "90", + Color::LightRed => "91", + Color::LightGreen => "92", + Color::LightYellow => "93", + Color::LightBlue => "94", + Color::LightMagenta => "95", + Color::LightCyan => "96", + Color::White => "97", + _ => "", + })?; + if let Color::Rgb(red, green, blue) = color { + f.write_fmt(format_args!("38;2;{red};{green};{blue}"))?; + } + if let Color::Indexed(i) = color { + f.write_fmt(format_args!("38;5;{i}"))?; + } + f.write_str(";") +} + +fn write_bg(f: &mut Formatter, color: Color) -> fmt::Result { + f.write_str(match color { + Color::Reset => "49", + Color::Black => "40", + Color::Red => "41", + Color::Green => "42", + Color::Yellow => "43", + Color::Blue => "44", + Color::Magenta => "45", + Color::Cyan => "46", + Color::Gray => "47", + Color::DarkGray => "100", + Color::LightRed => "101", + Color::LightGreen => "102", + Color::LightYellow => "103", + Color::LightBlue => "104", + Color::LightMagenta => "105", + Color::LightCyan => "106", + Color::White => "107", + _ => "", + })?; + if let Color::Rgb(red, green, blue) = color { + f.write_fmt(format_args!("48;2;{red};{green};{blue}"))?; + } + if let Color::Indexed(i) = color { + f.write_fmt(format_args!("48;5;{i}"))?; + } + Ok(()) +} diff --git a/packages/cli/src/serve/builder.rs b/packages/cli/src/serve/builder.rs deleted file mode 100644 index 79028b67d..000000000 --- a/packages/cli/src/serve/builder.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::builder::BuildRequest; -use crate::builder::BuildResult; -use crate::builder::TargetPlatform; -use crate::builder::UpdateBuildProgress; -use crate::dioxus_crate::DioxusCrate; -use crate::serve::next_or_pending; -use crate::serve::Serve; -use crate::Result; -use futures_channel::mpsc::UnboundedReceiver; -use futures_util::future::OptionFuture; -use futures_util::stream::select_all; -use futures_util::StreamExt; -use std::process::Stdio; -use tokio::{ - process::{Child, Command}, - task::JoinHandle, -}; - -/// A handle to ongoing builds and then the spawned tasks themselves -pub struct Builder { - /// The results of the build - build_results: Option>>>, - - /// The progress of the builds - build_progress: Vec<(TargetPlatform, UnboundedReceiver)>, - - /// The application we are building - config: DioxusCrate, - - /// The arguments for the build - serve: Serve, - - /// The children of the build process - pub children: Vec<(TargetPlatform, Child)>, -} - -impl Builder { - /// Create a new builder - pub fn new(config: &DioxusCrate, serve: &Serve) -> Self { - let serve = serve.clone(); - let config = config.clone(); - Self { - build_results: None, - build_progress: Vec::new(), - config: config.clone(), - serve, - children: Vec::new(), - } - } - - /// Start a new build - killing the current one if it exists - pub fn build(&mut self) -> Result<()> { - self.shutdown(); - let build_requests = - BuildRequest::create(true, &self.config, self.serve.build_arguments.clone())?; - - let mut set = tokio::task::JoinSet::new(); - - for build_request in build_requests { - let (mut tx, rx) = futures_channel::mpsc::unbounded(); - self.build_progress - .push((build_request.target_platform, rx)); - set.spawn(async move { - let res = build_request.build(tx.clone()).await; - - if let Err(err) = &res { - let _ = tx.start_send(UpdateBuildProgress { - stage: crate::builder::Stage::Finished, - update: crate::builder::UpdateStage::Failed(format!("{err}")), - }); - } - - res - }); - } - - self.build_results = Some(tokio::spawn(async move { - let mut all_results = Vec::new(); - while let Some(result) = set.join_next().await { - let res = result.map_err(|err| { - crate::Error::Unique(format!("Panic while building project: {err:?}")) - })??; - - all_results.push(res); - } - Ok(all_results) - })); - - Ok(()) - } - - /// Wait for any new updates to the builder - either it completed or gave us a message etc - pub async fn wait(&mut self) -> Result { - // Wait for build progress - let mut next = select_all( - self.build_progress - .iter_mut() - .map(|(platform, rx)| rx.map(move |update| (*platform, update))), - ); - let next = next_or_pending(next.next()); - - // The ongoing builds directly - let results: OptionFuture<_> = self.build_results.as_mut().into(); - let results = next_or_pending(results); - - // The process exits - let children_empty = self.children.is_empty(); - let process_exited = self - .children - .iter_mut() - .map(|(target, child)| Box::pin(async move { (*target, child.wait().await) })); - let process_exited = async move { - if children_empty { - return futures_util::future::pending().await; - } - futures_util::future::select_all(process_exited).await - }; - - // Wait for the next build result - tokio::select! { - build_results = results => { - self.build_results = None; - - // If we have a build result, bubble it up to the main loop - let build_results = build_results.map_err(|_| crate::Error::Unique("Build join failed".to_string()))??; - - Ok(BuilderUpdate::Ready { results: build_results }) - } - (platform, update) = next => { - // If we have a build progress, send it to the screen - Ok(BuilderUpdate::Progress { platform, update }) - } - ((target, exit_status), _, _) = process_exited => { - Ok(BuilderUpdate::ProcessExited { status: exit_status, target_platform: target }) - } - } - } - - /// Shutdown the current build process - pub(crate) fn shutdown(&mut self) { - for (_, mut child) in self.children.drain(..) { - // Gracefully shtudown the desktop app - // It might have a receiver to do some cleanup stuff - if let Some(pid) = child.id() { - // on unix, we can send a signal to the process to shut down - #[cfg(unix)] - { - _ = Command::new("kill") - .args(["-s", "TERM", &pid.to_string()]) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .spawn(); - } - - // on windows, use the `taskkill` command - #[cfg(windows)] - { - _ = Command::new("taskkill") - .args(["/F", "/PID", &pid.to_string()]) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .spawn(); - } - } - - // Todo: add a timeout here to kill the process if it doesn't shut down within a reasonable time - _ = child.start_kill(); - } - - if let Some(tasks) = self.build_results.take() { - tasks.abort(); - } - self.build_progress.clear(); - } -} - -pub enum BuilderUpdate { - Progress { - platform: TargetPlatform, - update: UpdateBuildProgress, - }, - Ready { - results: Vec, - }, - ProcessExited { - target_platform: TargetPlatform, - status: Result, - }, -} diff --git a/packages/cli/src/serve/detect.rs b/packages/cli/src/serve/detect.rs new file mode 100644 index 000000000..504731da1 --- /dev/null +++ b/packages/cli/src/serve/detect.rs @@ -0,0 +1,32 @@ +/// Detects if `dx` is being ran in a WSL environment. +/// +/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files. +/// This may fail in the future as it isn't guaranteed by Microsoft. +/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364 +pub(crate) fn is_wsl() -> bool { + const WSL_1: &str = "/proc/sys/kernel/osrelease"; + const WSL_2: &str = "/proc/version"; + const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"]; + + // Test 1st File + if let Ok(content) = std::fs::read_to_string(WSL_1) { + let lowercase = content.to_lowercase(); + for keyword in WSL_KEYWORDS { + if lowercase.contains(keyword) { + return true; + } + } + } + + // Test 2nd File + if let Ok(content) = std::fs::read_to_string(WSL_2) { + let lowercase = content.to_lowercase(); + for keyword in WSL_KEYWORDS { + if lowercase.contains(keyword) { + return true; + } + } + } + + false +} diff --git a/packages/cli/src/serve/handle.rs b/packages/cli/src/serve/handle.rs new file mode 100644 index 000000000..1aacd0c06 --- /dev/null +++ b/packages/cli/src/serve/handle.rs @@ -0,0 +1,451 @@ +use crate::{AppBundle, Platform, Result}; +use anyhow::Context; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + process::Stdio, +}; +use tokio::{ + io::{AsyncBufReadExt, BufReader, Lines}, + process::{Child, ChildStderr, ChildStdout, Command}, +}; + +/// A handle to a running app. +/// +/// Also includes a handle to its server if it exists. +/// The actual child processes might not be present (web) or running (died/killed). +/// +/// The purpose of this struct is to accumulate state about the running app and its server, like +/// any runtime information needed to hotreload the app or send it messages. +/// +/// We might want to bring in websockets here too, so we know the exact channels the app is using to +/// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super +/// duper useful. +pub(crate) struct AppHandle { + pub(crate) app: AppBundle, + + // These might be None if the app died or the user did not specify a server + pub(crate) app_child: Option, + pub(crate) server_child: Option, + + // stdio for the app so we can read its stdout/stderr + // we don't map stdin today (todo) but most apps don't need it + pub(crate) app_stdout: Option>>, + pub(crate) app_stderr: Option>>, + pub(crate) server_stdout: Option>>, + pub(crate) server_stderr: Option>>, + + /// The virtual directory that assets will be served from + /// Used mostly for apk/ipa builds since they live in simulator + pub(crate) runtime_asst_dir: Option, +} + +impl AppHandle { + pub async fn new(app: AppBundle) -> Result { + Ok(AppHandle { + app, + runtime_asst_dir: None, + app_child: None, + app_stderr: None, + app_stdout: None, + server_child: None, + server_stdout: None, + server_stderr: None, + }) + } + + pub(crate) async fn open( + &mut self, + devserver_ip: SocketAddr, + fullstack_address: Option, + open_browser: bool, + ) -> Result<()> { + if let Some(addr) = fullstack_address { + tracing::debug!("Proxying fullstack server from port {:?}", addr); + } + + // Set the env vars that the clients will expect + // These need to be stable within a release version (ie 0.6.0) + let mut envs = vec![ + ("DIOXUS_CLI_ENABLED", "true".to_string()), + ( + dioxus_cli_config::DEVSERVER_RAW_ADDR_ENV, + devserver_ip.to_string(), + ), + // unset the cargo dirs in the event we're running `dx` locally + // since the child process will inherit the env vars, we don't want to confuse the downstream process + ("CARGO_MANIFEST_DIR", "".to_string()), + ]; + + if let Some(addr) = fullstack_address { + envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string())); + envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string())); + } + + // Launch the server if we have one and consume its stdout/stderr + if let Some(server) = self.app.server_exe() { + tracing::debug!("Launching server from path: {server:?}"); + let mut child = Command::new(server) + .envs(envs.clone()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + let stdout = BufReader::new(child.stdout.take().unwrap()); + let stderr = BufReader::new(child.stderr.take().unwrap()); + self.server_stdout = Some(stdout.lines()); + self.server_stderr = Some(stderr.lines()); + self.server_child = Some(child); + } + + // We try to use stdin/stdout to communicate with the app + let running_process = match self.app.build.build.platform() { + // Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead + // use use the websocket to communicate with it. I wish we could merge the concepts here, + // like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else. + Platform::Web => { + // Only the first build we open the web app, after that the user knows it's running + if open_browser { + self.open_web(envs, devserver_ip); + } + + None + } + + Platform::Ios => Some(self.open_ios_sim(envs).await?), + + // https://developer.android.com/studio/run/emulator-commandline + Platform::Android => { + tracing::error!("Android is not yet supported, sorry!"); + None + } + + // These are all just basically running the main exe, but with slightly different resource dir paths + Platform::Server + | Platform::MacOS + | Platform::Windows + | Platform::Linux + | Platform::Liveview => Some(self.open_with_main_exe(envs)?), + }; + + // If we have a running process, we need to attach to it and wait for its outputs + if let Some(mut child) = running_process { + let stdout = BufReader::new(child.stdout.take().unwrap()); + let stderr = BufReader::new(child.stderr.take().unwrap()); + self.app_stdout = Some(stdout.lines()); + self.app_stderr = Some(stderr.lines()); + self.app_child = Some(child); + } + + Ok(()) + } + + /// Hotreload an asset in the running app. + /// + /// This will modify the build dir in place! Be careful! We generally assume you want all bundles + /// to reflect the latest changes, so we will modify the bundle. + /// + /// However, not all platforms work like this, so we might also need to update a separate asset + /// dir that the system simulator might be providing. We know this is the case for ios simulators + /// and haven't yet checked for android. + /// + /// This will return the bundled name of the asset such that we can send it to the clients letting + /// them know what to reload. It's not super important that this is robust since most clients will + /// kick all stylsheets without necessarily checking the name. + pub(crate) fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option { + let mut bundled_name = None; + + // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps, + // we won't actually be using the build dir. + let asset_dir = match self.runtime_asst_dir.as_ref() { + Some(dir) => dir.to_path_buf().join("assets/"), + None => self.app.asset_dir(), + }; + + tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}"); + + // If the asset shares the same name in the bundle, reload that + let legacy_asset_dir = self.app.build.krate.legacy_asset_dir(); + if changed_file.starts_with(&legacy_asset_dir) { + tracing::debug!("Hotreloading legacy asset {changed_file:?}"); + let trimmed = changed_file.strip_prefix(legacy_asset_dir).unwrap(); + let res = std::fs::copy(changed_file, asset_dir.join(trimmed)); + bundled_name = Some(trimmed.to_path_buf()); + if let Err(e) = res { + tracing::debug!("Failed to hotreload legacy asset {e}"); + } + } + + // The asset might've been renamed thanks to the manifest, let's attempt to reload that too + if let Some(resource) = self.app.app.assets.assets.get(changed_file).as_ref() { + let res = std::fs::copy(changed_file, asset_dir.join(&resource.bundled)); + bundled_name = Some(PathBuf::from(&resource.bundled)); + if let Err(e) = res { + tracing::debug!("Failed to hotreload asset {e}"); + } + } + + // Now we can return the bundled asset name to send to the hotreload engine + bundled_name + } + + /// Open the native app simply by running its main exe + /// + /// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths, + /// but for now, we just run the exe directly. Very few users should be caring about `dylib` search + /// paths right now, but they will when we start to enable things like swift integration. + /// + /// Server/liveview/desktop are all basically the same, though + fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result { + let child = Command::new(self.app.main_exe()) + .envs(envs) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + Ok(child) + } + + /// Open the web app by opening the browser to the given address. + /// Check if we need to use https or not, and if so, add the protocol. + /// Go to the basepath if that's set too. + fn open_web(&self, _envs: Vec<(&str, String)>, address: SocketAddr) { + let base_path = self.app.build.krate.config.web.app.base_path.clone(); + let https = self + .app + .build + .krate + .config + .web + .https + .enabled + .unwrap_or_default(); + let protocol = if https { "https" } else { "http" }; + let base_path = match base_path.as_deref() { + Some(base_path) => format!("/{}", base_path.trim_matches('/')), + None => "".to_owned(), + }; + _ = open::that(format!("{protocol}://{address}{base_path}")); + } + + /// Use `xcrun` to install the app to the simulator + /// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning + /// or entitlements, or anything like that. + /// + /// However, if there's no simulator running, this *might* fail. + /// + /// TODO(jon): we should probably check if there's a simulator running before trying to install, + /// and open the simulator if we have to. + async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result { + tracing::debug!("Installing app to simulator {:?}", self.app.app_dir()); + + let res = Command::new("xcrun") + .arg("simctl") + .arg("install") + .arg("booted") + .arg(self.app.app_dir()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .output() + .await?; + + tracing::debug!("Installed app to simulator with exit code: {res:?}"); + + // Remap the envs to the correct simctl env vars + // iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX" + let ios_envs = envs + .iter() + .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone())); + + let child = Command::new("xcrun") + .arg("simctl") + .arg("launch") + .arg("--console") + .arg("booted") + .arg("com.dioxuslabs") + .envs(ios_envs) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + tracing::debug!("Launched app on simulator with exit code: {child:?}"); + + Ok(child) + } + + /// We have this whole thing figured out, but we don't actually use it yet. + /// + /// Launching on devices is more complicated and requires us to codesign the app, which we don't + /// currently do. + /// + /// Converting these commands shouldn't be too hard, but device support would imply we need + /// better support for codesigning and entitlements. + #[allow(unused)] + async fn open_ios_device(&self) -> Result<()> { + // APP_PATH="target/aarch64-apple-ios/debug/bundle/ios/DioxusApp.app" + + // # get the device id by jq-ing the json of the device list + // xcrun devicectl list devices --json-output target/deviceid.json + // DEVICE_UUID=$(jq -r '.result.devices[0].identifier' target/deviceid.json) + + // xcrun devicectl device install app --device "${DEVICE_UUID}" "${APP_PATH}" --json-output target/xcrun.json + + // # get the installation url by jq-ing the json of the device install + // INSTALLATION_URL=$(jq -r '.result.installedApplications[0].installationURL' target/xcrun.json) + + // # launch the app + // # todo: we can just background it immediately and then pick it up for loading its logs + // xcrun devicectl device process launch --device "${DEVICE_UUID}" "${INSTALLATION_URL}" + + // # # launch the app and put it in background + // # xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}" --json-output "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}" + + // # # Extract background PID of status app + // # STATUS_PID=$(jq -r '.result.process.processIdentifier' "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}") + // # "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1 + + // # # now that metro is ready, resume the app from background + // # xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1 + + use serde_json::Value; + let app_path = self.app.app_dir(); + + install_app(&app_path).await?; + + // 2. Determine which device the app was installed to + let device_uuid = get_device_uuid().await?; + + // 3. Get the installation URL of the app + let installation_url = get_installation_url(&device_uuid, &app_path).await?; + + // 4. Launch the app into the background, paused + launch_app_paused(&device_uuid, &installation_url).await?; + + // 5. Pick up the paused app and resume it + resume_app(&device_uuid).await?; + + async fn install_app(app_path: &PathBuf) -> Result<()> { + let output = Command::new("xcrun") + .args(["simctl", "install", "booted"]) + .arg(app_path) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to install app: {:?}", output).into()); + } + + Ok(()) + } + + async fn get_device_uuid() -> Result { + let output = Command::new("xcrun") + .args([ + "devicectl", + "list", + "devices", + "--json-output", + "target/deviceid.json", + ]) + .output() + .await?; + + let json: Value = + serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?) + .context("Failed to parse xcrun output")?; + let device_uuid = json["result"]["devices"][0]["identifier"] + .as_str() + .ok_or("Failed to extract device UUID")? + .to_string(); + + Ok(device_uuid) + } + + async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result { + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "install", + "app", + "--device", + device_uuid, + &app_path.display().to_string(), + "--json-output", + "target/xcrun.json", + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to install app: {:?}", output).into()); + } + + let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?) + .context("Failed to parse xcrun output")?; + let installation_url = json["result"]["installedApplications"][0]["installationURL"] + .as_str() + .ok_or("Failed to extract installation URL")? + .to_string(); + + Ok(installation_url) + } + + async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> { + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "process", + "launch", + "--no-activate", + "--verbose", + "--device", + device_uuid, + installation_url, + "--json-output", + "target/launch.json", + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to launch app: {:?}", output).into()); + } + + Ok(()) + } + + async fn resume_app(device_uuid: &str) -> Result<()> { + let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?) + .context("Failed to parse xcrun output")?; + + let status_pid = json["result"]["process"]["processIdentifier"] + .as_u64() + .ok_or("Failed to extract process identifier")?; + + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "process", + "resume", + "--device", + device_uuid, + "--pid", + &status_pid.to_string(), + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to resume app: {:?}", output).into()); + } + + Ok(()) + } + + unimplemented!("dioxus-cli doesn't support ios devices yet.") + } +} diff --git a/packages/cli/src/serve/hot_reloading_file_map.rs b/packages/cli/src/serve/hot_reloading_file_map.rs deleted file mode 100644 index 579b3966d..000000000 --- a/packages/cli/src/serve/hot_reloading_file_map.rs +++ /dev/null @@ -1,313 +0,0 @@ -use dioxus_core::internal::{HotReloadTemplateWithLocation, HotReloadedTemplate}; -use dioxus_core_types::HotReloadingContext; -use dioxus_rsx::CallBody; -use dioxus_rsx_hotreload::{diff_rsx, ChangedRsx}; -use krates::cm::MetadataCommand; -use krates::Cmd; -pub use std::collections::HashMap; -use std::{ffi::OsStr, path::PathBuf}; -pub use std::{fs, io, path::Path}; -pub use std::{fs::File, io::Read}; -use syn::spanned::Spanned; - -pub struct FileMap { - pub map: HashMap, - - /// Any errors that occurred while building the FileMap that were not fatal - pub errors: Vec, - - pub in_workspace: HashMap>, -} - -/// A cached file that has been parsed -/// -/// We store the templates found in this file -pub struct CachedSynFile { - pub raw: String, - pub templates: HashMap, -} - -impl FileMap { - /// Create a new FileMap from a crate directory - /// - /// TODO: this should be created with a gitignore filter - pub fn create(path: PathBuf) -> io::Result { - Self::create_with_filter::(path, |p| { - // skip some stuff we know is large by default - p.file_name() == Some(OsStr::new("target")) - || p.file_name() == Some(OsStr::new("node_modules")) - }) - } - - /// Create a new FileMap from a crate directory - /// - /// Takes a filter that when returns true, the file will be filtered out (ie not tracked) - /// Note that this is inverted from a typical .filter() method. - pub fn create_with_filter( - crate_dir: PathBuf, - mut filter: impl FnMut(&Path) -> bool, - ) -> io::Result { - let FileMapSearchResult { map, errors } = find_rs_files(crate_dir.clone(), &mut filter); - - let mut map = Self { - map, - errors, - in_workspace: HashMap::new(), - }; - - map.load_assets::(crate_dir.as_path()); - - Ok(map) - } - - /// Start watching assets for changes - /// - /// This just diffs every file against itself and populates the tracked assets as it goes - pub fn load_assets(&mut self, crate_dir: &Path) { - let keys = self.map.keys().cloned().collect::>(); - for file in keys { - _ = self.update_rsx::(file.as_path(), crate_dir); - } - } - - /// Insert a file into the map and force a full rebuild - fn full_rebuild(&mut self, file_path: PathBuf, src: String) -> HotreloadError { - let cached_file = CachedSynFile { - raw: src.clone(), - templates: HashMap::new(), - }; - - self.map.insert(file_path, cached_file); - HotreloadError::Notreloadable - } - - /// Try to update the rsx in a file - pub fn update_rsx( - &mut self, - file_path: &Path, - crate_dir: &Path, - ) -> Result, HotreloadError> { - let src = std::fs::read_to_string(file_path)?; - - // If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error - let syntax = syn::parse_file(&src).map_err(|_err| HotreloadError::Parse)?; - - let in_workspace = self.child_in_workspace(crate_dir)?; - - // Get the cached file if it exists, otherwise try to create it - let Some(old_cached) = self.map.get_mut(file_path) else { - // if this is a new file, rebuild the project - let mut map = FileMap::create::(crate_dir.to_path_buf())?; - - if let Some(err) = map.errors.pop() { - return Err(HotreloadError::Failure(err)); - } - - // merge the new map into the old map - self.map.extend(map.map); - - return Err(HotreloadError::Notreloadable); - }; - - // If the cached file is not a valid rsx file, rebuild the project, forcing errors - // TODO: in theory the error is simply in the RsxCallbody. We could attempt to parse it using partial expansion - // And collect out its errors instead of giving up to a full rebuild - let old = syn::parse_file(&old_cached.raw).map_err(|_e| HotreloadError::Parse)?; - - let instances = match diff_rsx(&syntax, &old) { - // If the changes were just some rsx, we can just update the template - // - // However... if the changes involved code in the rsx itself, this should actually be a CodeChanged - Some(rsx_calls) => rsx_calls, - - // If the changes were some code, we should insert the file into the map and rebuild - // todo: not sure we even need to put the cached file into the map, but whatever - None => { - return Err(self.full_rebuild(file_path.to_path_buf(), src)); - } - }; - - let mut out_templates = vec![]; - - for calls in instances.into_iter() { - let ChangedRsx { old, new } = calls; - - let old_start = old.span().start(); - - let old_parsed = syn::parse2::(old.tokens); - let new_parsed = syn::parse2::(new.tokens); - let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else { - continue; - }; - - // if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root - // we need to check if the file is in a workspace or not and strip the prefix accordingly - let prefix = match in_workspace { - Some(ref workspace) => workspace, - _ => crate_dir, - }; - - let Ok(file) = file_path.strip_prefix(prefix) else { - continue; - }; - - let template_location = template_location(old_start, file); - - // Returns a list of templates that are hotreloadable - let hotreload_result = dioxus_rsx_hotreload::HotReloadResult::new::( - &old_call_body.body, - &new_call_body.body, - template_location.clone(), - ); - - // if the template is not hotreloadable, we need to do a full rebuild - let Some(mut results) = hotreload_result else { - return Err(self.full_rebuild(file_path.to_path_buf(), src)); - }; - - // Be careful to not send the bad templates - results.templates.retain(|idx, template| { - // dioxus cannot handle empty templates... - if template.roots.is_empty() { - return false; - } - let template_location = format_template_name(&template_location, *idx); - - // if the template is the same, don't send its - if old_cached.templates.get(&template_location) == Some(&*template) { - return false; - }; - - // Update the most recent idea of the template - // This lets us know if the template has changed so we don't need to send it - old_cached - .templates - .insert(template_location, template.clone()); - - true - }); - - out_templates.extend(results.templates.into_iter().map(|(idx, template)| { - HotReloadTemplateWithLocation { - location: format_template_name(&template_location, idx), - template, - } - })); - } - - Ok(out_templates) - } - - fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result> { - if let Some(in_workspace) = self.in_workspace.get(crate_dir) { - return Ok(in_workspace.clone()); - } - - let mut cmd = Cmd::new(); - let manafest_path = crate_dir.join("Cargo.toml"); - cmd.manifest_path(&manafest_path); - let cmd: MetadataCommand = cmd.into(); - let metadata = cmd - .exec() - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - - let in_workspace = metadata.workspace_root != crate_dir; - let workspace_path = in_workspace.then(|| metadata.workspace_root.into()); - self.in_workspace - .insert(crate_dir.to_path_buf(), workspace_path.clone()); - Ok(workspace_path) - } -} - -pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String { - let line = old_start.line; - let column = old_start.column + 1; - - // Always ensure the path components are separated by `/`. - let path = file - .components() - .map(|c| c.as_os_str().to_string_lossy()) - .collect::>() - .join("/"); - - path + ":" + line.to_string().as_str() + ":" + column.to_string().as_str() -} - -pub fn format_template_name(name: &str, index: usize) -> String { - format!("{}:{}", name, index) -} - -struct FileMapSearchResult { - map: HashMap, - errors: Vec, -} - -// todo: we could just steal the mod logic from rustc itself -fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileMapSearchResult { - let mut files = HashMap::new(); - let mut errors = Vec::new(); - - if root.is_dir() { - let read_dir = match fs::read_dir(root) { - Ok(read_dir) => read_dir, - Err(err) => { - errors.push(err); - return FileMapSearchResult { map: files, errors }; - } - }; - for entry in read_dir.flatten() { - let path = entry.path(); - if !filter(&path) { - let FileMapSearchResult { - map, - errors: child_errors, - } = find_rs_files(path, filter); - errors.extend(child_errors); - files.extend(map); - } - } - } else if root.extension().and_then(|s| s.to_str()) == Some("rs") { - if let Ok(mut file) = File::open(root.clone()) { - let mut src = String::new(); - match file.read_to_string(&mut src) { - Ok(_) => { - let cached_file = CachedSynFile { - raw: src.clone(), - templates: HashMap::new(), - }; - - // track assets while we're here - files.insert(root, cached_file); - } - Err(err) => { - errors.push(err); - } - } - } - } - - FileMapSearchResult { map: files, errors } -} - -#[derive(Debug)] -pub enum HotreloadError { - Failure(io::Error), - Parse, - Notreloadable, -} - -impl std::fmt::Display for HotreloadError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Failure(err) => write!(f, "Failed to parse file: {}", err), - Self::Parse => write!(f, "Failed to parse file"), - Self::Notreloadable => write!(f, "Template is not hotreloadable"), - } - } -} - -impl From for HotreloadError { - fn from(err: io::Error) -> Self { - HotreloadError::Failure(err) - } -} diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index f12aced97..a12a46f38 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -1,253 +1,258 @@ -use std::future::{poll_fn, Future, IntoFuture}; -use std::task::Poll; - -use crate::builder::OpenArguments; -use crate::cli::serve::Serve; -use crate::dioxus_crate::DioxusCrate; -use crate::tracer::CLILogControl; -use crate::Result; use crate::{ - builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage}, + BuildUpdate, Builder, DioxusCrate, Error, Platform, Result, ServeArgs, TraceController, TraceSrc, }; -use futures_util::FutureExt; -use tokio::task::yield_now; -mod builder; -mod hot_reloading_file_map; -mod logs_tab; +mod ansi_buffer; +mod detect; +mod handle; mod output; mod proxy; -mod render; +mod runner; mod server; +mod update; mod watcher; -use builder::*; -use output::*; -use server::*; -use watcher::*; +pub(crate) use handle::*; +pub(crate) use output::*; +pub(crate) use runner::*; +pub(crate) use server::*; +pub(crate) use update::*; +pub(crate) use watcher::*; -/// For *all* builds the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project. +/// For *all* builds, the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project. /// /// This includes web, desktop, mobile, fullstack, etc. /// /// Platform specifics: -/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We -/// want to emulate GithubPages here since most folks are deploying there and expect things like -/// basepath to match. -/// - Fullstack: We spin up the same dev server but in this case the fullstack server itself needs to -/// proxy all dev requests to our dev server -/// - Desktop: We spin up the dev server but without a filesystem server. -/// - Mobile: Basically the same as desktop. +/// ------------------- +/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We +/// want to emulate GithubPages here since most folks are deploying there and expect things like +/// basepath to match. +/// - Desktop: We spin up the dev server but without a filesystem server. +/// - Mobile: Basically the same as desktop. /// -/// Notes: -/// - All filesystem changes are tracked here -/// - We send all updates to connected websocket connections. Even desktop connects via the websocket -/// - Right now desktop compiles tokio-tungstenite to do the connection but we could in theory reuse -/// the websocket logic from the webview for thinner builds. +/// When fullstack is enabled, we'll also build for the `server` target and then hotreload the server. +/// The "server" is special here since "fullstack" is functionally just an addition to the regular client +/// setup. /// /// Todos(Jon): -/// - I'd love to be able to configure the CLI while it's running so we can change settingaon the fly. -/// This would require some light refactoring and potentially pulling in something like ratatui. -/// - Build a custom subscriber for logs by tools within this -/// - Handle logs from the build engine separately? -/// - Consume logs from the wasm for web/fullstack +/// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly. /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server /// to a dynamic one on the fly. -pub async fn serve_all( - serve: Serve, - dioxus_crate: DioxusCrate, - log_control: CLILogControl, -) -> Result<()> { - // Start the screen first so we collect build logs. - let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger"); - let mut builder = Builder::new(&dioxus_crate, &serve); +pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()> { + let mut tracer = TraceController::redirect(); - // Start the first build - builder.build()?; + // Note that starting the builder will queue up a build immediately + let mut builder = Builder::start(&krate, args.build_args())?; + let mut devserver = WebServer::start(&krate, &args)?; + let mut watcher = Watcher::start(&krate, &args); + let mut runner = AppRunner::start(&krate); + let mut screen = Output::start(&args)?; - let mut server = Server::start(&serve, &dioxus_crate); - let mut watcher = Watcher::start(&serve, &dioxus_crate); + // This is our default splash screen. We might want to make this a fancier splash screen in the future + // Also, these commands might not be the most important, but it's all we've got enabled right now + tracing::info!( + r#"Serving your Dioxus app: {} 🚀 - let is_hot_reload = serve.server_arguments.hot_reload.unwrap_or(true); + - Press `ctrl+c` to exit the server + - Press `r` to rebuild the app + - Press `o` to open the app + - Press `t` to toggle cargo output + - Press `/` for more commands and shortcuts - loop { - // Make sure we don't hog the CPU: these loop { select! {} } blocks can starve the executor - yield_now().await; + Learn more at https://dioxuslabs.com/learn/0.6/getting_started"#, + krate.executable_name() + ); + let err: Result<(), Error> = loop { // Draw the state of the server to the screen - screen.render(&serve, &dioxus_crate, &builder, &server, &watcher); + screen.render(&args, &krate, &builder, &devserver, &watcher); // And then wait for any updates before redrawing - tokio::select! { - // rebuild the project or hotreload it - _ = watcher.wait(), if is_hot_reload => { - if !watcher.pending_changes() { - continue + let msg = tokio::select! { + msg = builder.wait() => ServeUpdate::BuildUpdate(msg), + msg = watcher.wait() => msg, + msg = devserver.wait() => msg, + msg = screen.wait() => msg, + msg = runner.wait() => msg, + msg = tracer.wait() => msg, + }; + + match msg { + ServeUpdate::FilesChanged { files } => { + if files.is_empty() || !args.should_hotreload() { + continue; } - let changed_files = watcher.dequeue_changed_files(&dioxus_crate); - let changed = changed_files.first().cloned(); + let file = files[0].display().to_string(); + let file = file.trim_start_matches(&krate.crate_dir().display().to_string()); // if change is hotreloadable, hotreload it // and then send that update to all connected clients - if let Some(hr) = watcher.attempt_hot_reload(&dioxus_crate, changed_files) { + if let Some(hr) = runner.attempt_hot_reload(files) { // Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild - if hr.templates.is_empty() && hr.assets.is_empty() && hr.unknown_files.is_empty() { - continue + if hr.templates.is_empty() + && hr.assets.is_empty() + && hr.unknown_files.is_empty() + { + tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file); + continue; } - if let Some(changed_path) = changed { - let path_relative = changed_path.strip_prefix(dioxus_crate.crate_dir()).map(|p| p.display().to_string()).unwrap_or_else(|_| changed_path.display().to_string()); - tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloaded {}", path_relative); - } + tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {}", file); + + devserver.send_hotreload(hr).await; + } else if runner.should_full_rebuild { + tracing::info!(dx_src = ?TraceSrc::Dev, "Full rebuild: {}", file); - server.send_hotreload(hr).await; - } else { - // If the change is not binary patchable, rebuild the project // We're going to kick off a new build, interrupting the current build if it's ongoing - builder.build()?; + builder.rebuild(args.build_arguments.clone()); - // Clear the hot reload changes - watcher.clear_hot_reload_changes(); + // Clear the hot reload changes so we don't have out-of-sync issues with changed UI + runner.clear_hot_reload_changes(); + runner.file_map.force_rebuild(); // Tell the server to show a loading page for any new requests - server.start_build().await; + devserver.start_build().await; + } else { + tracing::warn!( + "Rebuild required but is currently paused - press `r` to rebuild manually" + ) } } - // reload the page - msg = server.wait() => { - // Run the server in the background - // Waiting for updates here lets us tap into when clients are added/removed - match msg { - Some(ServerUpdate::NewConnection) => { - if let Some(msg) = watcher.applied_hot_reload_changes() { - server.send_hotreload(msg).await; - } - } - Some(ServerUpdate::Message(msg)) => { - screen.new_ws_message(TargetPlatform::Web, msg); - } - None => {} - } + // Run the server in the background + // Waiting for updates here lets us tap into when clients are added/removed + ServeUpdate::NewConnection => { + devserver + .send_hotreload(runner.applied_hot_reload_changes()) + .await; + + runner.client_connected().await; } - // Handle updates from the build engine - application = builder.wait() => { - // Wait for logs from the build engine - // These will cause us to update the screen - // We also can check the status of the builds here in case we have multiple ongoing builds - match application { - Ok(BuilderUpdate::Progress { platform, update }) => { - let update_clone = update.clone(); - screen.new_build_progress(platform, update_clone); - server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await; + // Received a message from the devtools server - currently we only use this for + // logging, so we just forward it the tui + ServeUpdate::WsMessage(msg) => { + screen.push_ws_message(Platform::Web, msg); + } - match update { - // Send rebuild start message. - UpdateBuildProgress { stage: Stage::Compiling, update: UpdateStage::Start } => server.send_reload_start().await, - // Send rebuild failed message. - UpdateBuildProgress { stage: Stage::Finished, update: UpdateStage::Failed(_) } => server.send_reload_failed().await, - _ => {}, - } + // Wait for logs from the build engine + // These will cause us to update the screen + // We also can check the status of the builds here in case we have multiple ongoing builds + ServeUpdate::BuildUpdate(update) => { + // Queue any logs to be printed if need be + screen.new_build_update(&update); + + // And then update the websocketed clients with the new build status in case they want it + devserver.new_build_update(&update).await; + + // And then open the app if it's ready + // todo: there might be more things to do here that require coordination with other pieces of the CLI + // todo: maybe we want to shuffle the runner around to send an "open" command instead of doing that + match update { + BuildUpdate::Progress { .. } => {} + BuildUpdate::CompilerMessage { message } => { + screen.push_cargo_log(message); } - Ok(BuilderUpdate::Ready { results }) => { - if !results.is_empty() { - builder.children.clear(); - } - - // If we have a build result, open it - for build_result in results.iter() { - let child = build_result.open( - OpenArguments::new( - &serve.server_arguments, - server.fullstack_address(), - &dioxus_crate + BuildUpdate::BuildFailed { err } => { + tracing::error!("Build failed: {}", err); + } + BuildUpdate::BuildReady { bundle } => { + let handle = runner + .open( + bundle, + devserver.devserver_address(), + devserver.proxied_server_address(), + args.open.unwrap_or(false), ) - ); - match child { - Ok(Some(child_proc)) => builder.children.push((build_result.target_platform, child_proc)), - Err(e) => { - tracing::error!(dx_src = ?TraceSrc::Build, "Failed to open build result: {e}"); - break; - }, - _ => {} + .await; + + match handle { + // Update the screen + devserver with the new handle info + Ok(_handle) => { + devserver.send_reload_command().await; } + + Err(e) => tracing::error!("Failed to open app: {}", e), } - - // Make sure we immediately capture the stdout/stderr of the executable - - // otherwise it'll clobber our terminal output - screen.new_ready_app(&mut builder, results); - - // And then finally tell the server to reload - server.send_reload_command().await; - }, - - // If the desktop process exited *cleanly*, we can exit - Ok(BuilderUpdate::ProcessExited { status, target_platform }) => { - // Then remove the child process - builder.children.retain(|(platform, _)| *platform != target_platform); - match (target_platform, status) { - (TargetPlatform::Desktop, Ok(status)) => { - if status.success() { - break; - } - else { - tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with status: {status}"); - } - }, - // Ignore the static generation platform exiting - (_ , Ok(_)) => {}, - (_, Err(e)) => { - tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with error: {e}"); - } - } - } - Err(err) => { - server.send_build_error(err).await; } } } - // Handle input from the user using our settings - res = screen.wait() => { - match res { - Ok(false) => {} - // Request a rebuild. - Ok(true) => { - builder.build()?; - server.start_build().await - }, - // Shutdown the server. - Err(_) => break, + // If the process exited *cleanly*, we can exit + ServeUpdate::ProcessExited { status, platform } => { + if !status.success() { + tracing::error!("Application [{platform}] exited with error: {status}"); + } else { + tracing::info!( + r#"Application [{platform}] exited gracefully. + - To restart the app, press `r` to rebuild or `o` to open + - To exit the server, press `ctrl+c`"# + ); } + + runner.kill(platform); } + + ServeUpdate::StdoutReceived { platform, msg } => { + screen.push_stdio(platform, msg, tracing::Level::INFO); + } + + ServeUpdate::StderrReceived { platform, msg } => { + screen.push_stdio(platform, msg, tracing::Level::ERROR); + } + + ServeUpdate::TracingLog { log } => { + screen.push_log(log); + } + + ServeUpdate::RequestRebuild => { + // The spacing here is important-ish: we want + // `Full rebuild:` to line up with + // `Hotreloading:` to keep the alignment during long edit sessions + tracing::info!("Full rebuild: triggered manually"); + builder.rebuild(args.build_arguments.clone()); + runner.file_map.force_rebuild(); + devserver.start_build().await + } + + ServeUpdate::OpenApp => { + runner.open_existing(&devserver).await; + } + + ServeUpdate::Redraw => { + // simply returning will cause a redraw + } + + ServeUpdate::ToggleShouldRebuild => { + runner.should_full_rebuild = !runner.should_full_rebuild; + tracing::info!( + "Automatic rebuilds are currently: {}", + if runner.should_full_rebuild { + "enabled" + } else { + "disabled" + } + ) + } + + ServeUpdate::Exit { error } => match error { + Some(err) => break Err(anyhow::anyhow!("{}", err).into()), + None => break Ok(()), + }, } - } + }; - // Run our cleanup logic here - maybe printing as we go? - // todo: more printing, logging, error handling in this phase + _ = devserver.shutdown().await; _ = screen.shutdown(); - _ = server.shutdown().await; - builder.shutdown(); + builder.abort_all(); + tracer.shutdown(); + + if let Err(err) = err { + eprintln!("Exiting with error: {}", err); + } Ok(()) } - -// Grab the output of a future that returns an option or wait forever -pub(crate) fn next_or_pending(f: F) -> impl Future -where - F: IntoFuture>, -{ - let pinned = f.into_future().fuse(); - let mut pinned = Box::pin(pinned); - poll_fn(move |cx| { - let next = pinned.as_mut().poll(cx); - match next { - Poll::Ready(Some(next)) => Poll::Ready(next), - _ => Poll::Pending, - } - }) - .fuse() -} diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 1a34ae56c..5a6329229 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -1,822 +1,956 @@ -use crate::config::{AddressArguments, Platform}; use crate::{ - builder::{BuildResult, UpdateStage}, - builder::{Stage, TargetPlatform, UpdateBuildProgress}, - dioxus_crate::DioxusCrate, - serve::next_or_pending, - serve::Serve, - serve::{Builder, Server, Watcher}, - tracer::CLILogControl, - TraceMsg, TraceSrc, + serve::{ansi_buffer::AnsiStringBuffer, Builder, ServeUpdate, Watcher, WebServer}, + BuildStage, BuildUpdate, DioxusCrate, Platform, ServeArgs, TraceContent, TraceMsg, TraceSrc, }; use crossterm::{ cursor::{Hide, Show}, - event::{Event, EventStream, KeyCode, KeyEventKind, KeyModifiers}, - terminal::{ - disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, - LeaveAlternateScreen, + event::{ + DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, Event, + EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, }, - tty::IsTty, + terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, ExecutableCommand, }; -use dioxus_devtools::ClientMsg; -use futures_util::{future::select_all, Future, FutureExt, StreamExt}; -use ratatui::{prelude::*, TerminalOptions, Viewport}; +use ratatui::{ + prelude::*, + widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap}, + TerminalOptions, Viewport, +}; use std::{ cell::RefCell, - collections::{HashMap, HashSet}, + collections::VecDeque, io::{self, stdout}, rc::Rc, - sync::atomic::Ordering, - time::{Duration, Instant}, -}; -use tokio::{ - io::{AsyncBufReadExt, BufReader, Lines}, - process::{ChildStderr, ChildStdout}, + time::Duration, }; use tracing::Level; -use super::render; - -// How many lines should be scroll on each mouse scroll or arrow key input. -const SCROLL_SPEED: u16 = 2; -// Speed added to `SCROLL_SPEED` when the modifier key is held during scroll. -const SCROLL_MODIFIER: u16 = 4; -// Scroll modifier key. -const SCROLL_MODIFIER_KEY: KeyModifiers = KeyModifiers::SHIFT; - -#[derive(Default)] -pub struct BuildProgress { - pub(crate) current_builds: HashMap, -} - -impl BuildProgress { - pub fn progress(&self) -> f64 { - self.current_builds - .values() - .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) - .map(|build| match build.stage { - Stage::Initializing => 0.0, - Stage::InstallingWasmTooling => 0.0, - Stage::Compiling => build.progress, - Stage::OptimizingWasm | Stage::OptimizingAssets | Stage::Finished => 1.0, - }) - .unwrap_or_default() - } -} +const TICK_RATE_MS: u64 = 100; +const VIEWPORT_MAX_WIDTH: u16 = 100; +const VIEWPORT_HEIGHT_SMALL: u16 = 5; +const VIEWPORT_HEIGHT_BIG: u16 = 12; +/// The TUI that drives the console output. +/// +/// We try not to store too much state about the world here, just the state about the tui itself. +/// This is to prevent out-of-sync issues with the rest of the build engine and to use the components +/// of the serve engine as the source of truth. +/// +/// Please please, do not add state here that does not belong here. We should only be storing state +/// here that is used to change how we display *other* state. Things like throbbers, modals, etc. pub struct Output { - term: Rc>>, - log_control: CLILogControl, - - // optional since when there's no tty there's no eventstream to read from - just stdin + term: Rc>>>>, events: Option, - pub(crate) build_progress: BuildProgress, - running_apps: HashMap, - // A list of all messages from build, dev, app, and more. - messages: Vec, - - num_lines_wrapping: u16, - scroll_position: u16, - console_width: u16, - console_height: u16, - more_modal_open: bool, - anim_start: Instant, - interactive: bool, - _is_cli_release: bool, platform: Platform, - addr: AddressArguments, - // Filters - show_filter_menu: bool, - filters: Vec<(String, bool)>, - selected_filter_index: usize, - filter_search_mode: bool, - filter_search_input: Option, + // Whether to show verbose logs or not + // We automatically hide "debug" logs if verbose is false (only showing "info" / "warn" / "error") + verbose: bool, + trace: bool, - _rustc_version: String, - _rustc_nightly: bool, - _dx_version: String, + // Pending logs + pending_logs: VecDeque, + + dx_version: String, + tick_animation: bool, + + tick_interval: tokio::time::Interval, + + // ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our + // "render" method only borrows &self (for no particular reason at all...) + throbber: RefCell, } -type TerminalBackend = Terminal>; +#[allow(unused)] +#[derive(Clone, Copy)] +struct RenderState<'a> { + opts: &'a ServeArgs, + krate: &'a DioxusCrate, + build_engine: &'a Builder, + server: &'a WebServer, + watcher: &'a Watcher, +} impl Output { - pub fn start(cfg: &Serve, log_control: CLILogControl) -> io::Result { - let interactive = std::io::stdout().is_tty() && cfg.interactive.unwrap_or(true); - - let mut events = None; - - if interactive { - log_control.output_enabled.store(true, Ordering::SeqCst); - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?.execute(Hide)?; - - // workaround for ci where the terminal is not fully initialized - // this stupid bug - // https://github.com/crossterm-rs/crossterm/issues/659 - events = Some(EventStream::new()); - }; - - // set the panic hook to fix the terminal - set_fix_term_hook(); - - // Fix the vscode scrollback issue - fix_xtermjs_scrollback(); - - let term: Option = Terminal::with_options( - CrosstermBackend::new(stdout()), - TerminalOptions { - viewport: Viewport::Fullscreen, - }, - ) - .ok(); - - // todo: re-enable rustc version - // let rustc_version = rustc_version().await; - // let rustc_nightly = rustc_version.contains("nightly") || cfg.target_args.nightly; - let _rustc_version = String::from("1.0.0"); - let _rustc_nightly = false; - - let mut dx_version = String::new(); - - dx_version.push_str(env!("CARGO_PKG_VERSION")); - - // todo: we want the binstalls / cargo installs to be exempt, but installs from git are not - let is_cli_release = crate::dx_build_info::PROFILE == "release"; - if !is_cli_release { - if let Some(hash) = crate::dx_build_info::GIT_COMMIT_HASH_SHORT { - let hash = &hash.trim_start_matches('g')[..4]; - dx_version.push('-'); - dx_version.push_str(hash); - } - } - - let platform = cfg.build_arguments.platform.expect("To be resolved by now"); - - Ok(Self { - term: Rc::new(RefCell::new(term)), - log_control, - events, - _rustc_version, - _rustc_nightly, - _dx_version: dx_version, - interactive, - _is_cli_release: is_cli_release, - platform, - messages: Vec::new(), + pub(crate) fn start(cfg: &ServeArgs) -> io::Result { + let mut output = Self { + term: Rc::new(RefCell::new( + Terminal::with_options( + CrosstermBackend::new(stdout()), + TerminalOptions { + viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL), + }, + ) + .ok(), + )), + interactive: cfg.is_interactive_tty(), + dx_version: format!( + "{}-{}", + env!("CARGO_PKG_VERSION"), + crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("main") + ), + platform: cfg.build_arguments.platform.expect("To be resolved by now"), + events: None, + // messages: Vec::new(), more_modal_open: false, - build_progress: Default::default(), - running_apps: HashMap::new(), - scroll_position: 0, - num_lines_wrapping: 0, - console_width: 0, - console_height: 0, - anim_start: Instant::now(), - addr: cfg.server_arguments.address.clone(), - - // Filter - show_filter_menu: false, - filters: Vec::new(), - selected_filter_index: 0, - filter_search_input: None, - filter_search_mode: false, - }) - } - - /// Add a message from stderr to the logs - fn push_stderr(&mut self, platform: TargetPlatform, stderr: String) { - self.running_apps - .get_mut(&platform) - .unwrap() - .output - .as_mut() - .unwrap() - .stderr_line - .push_str(&stderr); - - self.messages.push(TraceMsg { - source: TraceSrc::App(platform), - level: Level::ERROR, - content: stderr, - }); - - if self.is_snapped() { - self.scroll_to_bottom(); - } - } - - /// Add a message from stdout to the logs - fn push_stdout(&mut self, platform: TargetPlatform, stdout: String) { - self.running_apps - .get_mut(&platform) - .unwrap() - .output - .as_mut() - .unwrap() - .stdout_line - .push_str(&stdout); - - self.messages.push(TraceMsg { - source: TraceSrc::App(platform), - level: Level::INFO, - content: stdout, - }); - - if self.is_snapped() { - self.scroll_to_bottom(); - } - } - - /// Wait for either the ctrl_c handler or the next event - /// - /// Why is the ctrl_c handler here? - /// - /// Also tick animations every few ms - pub async fn wait(&mut self) -> io::Result { - fn ok_and_some(f: F) -> impl Future - where - F: Future, E>>, - { - next_or_pending(async move { f.await.ok().flatten() }) - } - let user_input = async { - let events = self.events.as_mut()?; - events.next().await - }; - let user_input = ok_and_some(user_input.map(|e| e.transpose())); - - let has_running_apps = !self.running_apps.is_empty(); - let next_stdout = self.running_apps.values_mut().map(|app| { - let future = async move { - let (stdout, stderr) = match &mut app.output { - Some(out) => ( - ok_and_some(out.stdout.next_line()), - ok_and_some(out.stderr.next_line()), - ), - None => return futures_util::future::pending().await, - }; - - tokio::select! { - line = stdout => (app.result.target_platform, Some(line), None), - line = stderr => (app.result.target_platform, None, Some(line)), - } - }; - Box::pin(future) - }); - - let next_stdout = async { - if has_running_apps { - select_all(next_stdout).await.0 - } else { - futures_util::future::pending().await - } - }; - - let tui_log_rx = &mut self.log_control.output_rx; - let next_tui_log = next_or_pending(tui_log_rx.next()); - - tokio::select! { - (platform, stdout, stderr) = next_stdout => { - if let Some(stdout) = stdout { - self.push_stdout(platform, stdout); - } - if let Some(stderr) = stderr { - self.push_stderr(platform, stderr); - } + pending_logs: VecDeque::new(), + throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()), + trace: cfg.trace, + verbose: cfg.verbose, + tick_animation: false, + tick_interval: { + let mut interval = tokio::time::interval(Duration::from_millis(TICK_RATE_MS)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + interval }, + }; - // Handle internal CLI tracing logs. - log = next_tui_log => { - self.push_log(log); - } + output.startup()?; - event = user_input => { - if self.handle_events(event).await? { - return Ok(true) - } - } - } - - Ok(false) + Ok(output) } - pub fn shutdown(&mut self) -> io::Result<()> { - // if we're a tty then we need to disable the raw mode + /// Call the startup functions that might mess with the terminal settings. + /// This is meant to be paired with "shutdown" to restore the terminal to its original state. + fn startup(&mut self) -> io::Result<()> { if self.interactive { - self.log_control - .output_enabled - .store(false, Ordering::SeqCst); - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?.execute(Show)?; - self.drain_print_logs(); + // set the panic hook to fix the terminal in the event of a panic + // The terminal might be left in a wonky state if a panic occurs, and we don't want it to be completely broken + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + _ = disable_raw_mode(); + _ = stdout().execute(Show); + original_hook(info); + })); + + enable_raw_mode()?; + stdout() + .execute(Hide)? + .execute(EnableFocusChange)? + .execute(EnableBracketedPaste)?; + + // Initialize the event stream here - this is optional because an EvenStream in a non-interactive + // terminal will cause a panic instead of simply doing nothing. + // https://github.com/crossterm-rs/crossterm/issues/659 + self.events = Some(EventStream::new()); } Ok(()) } - /// Emit the build logs as println! statements such that the terminal has the same output as cargo - /// - /// This is used when the terminal is shutdown and we want the build logs in the terminal. Old - /// versions of the cli would just eat build logs making debugging issues harder than they needed - /// to be. - fn drain_print_logs(&mut self) { - let messages = self.messages.drain(..); + /// Call the shutdown functions that might mess with the terminal settings - see the related code + /// in "startup" for more details about what we need to unset + pub(crate) fn shutdown(&self) -> io::Result<()> { + if self.interactive { + stdout() + .execute(Show)? + .execute(DisableFocusChange)? + .execute(DisableBracketedPaste)?; + disable_raw_mode()?; - for msg in messages { - // TODO: Better formatting for different content lengths. - if msg.source != TraceSrc::Cargo { - println!("[{}] {}: {}", msg.source, msg.level, msg.content); - } else { - println!("{}", msg.content); + // print a few lines to not cut off the output + for _ in 0..3 { + println!(); + } + } + + Ok(()) + } + + pub(crate) async fn wait(&mut self) -> ServeUpdate { + use futures_util::future::OptionFuture; + use futures_util::StreamExt; + + // Wait for the next user event or animation tick + loop { + let next = OptionFuture::from(self.events.as_mut().map(|f| f.next())); + let event = tokio::select! { + biased; // Always choose the event over the animation tick to not lose the event + Some(Some(Ok(event))) = next => event, + _ = self.tick_interval.tick(), if self.tick_animation => { + self.throbber.borrow_mut().calc_next(); + return ServeUpdate::Redraw + }, + else => futures_util::future::pending().await + }; + + match self.handle_input(event) { + Ok(Some(update)) => return update, + Err(ee) => { + return ServeUpdate::Exit { + error: Some(Box::new(ee)), + } + } + Ok(None) => {} } } } /// Handle an input event, returning `true` if the event should cause the program to restart. - pub fn handle_input(&mut self, input: Event) -> io::Result { + fn handle_input(&mut self, input: Event) -> io::Result> { // handle ctrlc if let Event::Key(key) = input { if let KeyCode::Char('c') = key.code { if key.modifiers.contains(KeyModifiers::CONTROL) { - return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl-C")); + return Ok(Some(ServeUpdate::Exit { error: None })); } } } - // If we're in filter search mode we must capture all key inputs. - // This also handles when a filter is submitted. - if self.filter_search_mode { - if let Event::Key(key) = input { - if key.kind != KeyEventKind::Press { - return Ok(false); - } - - match key.code { - KeyCode::Char(c) => { - if let Some(input) = self.filter_search_input.as_mut() { - input.push(c); - } else { - self.filter_search_input = Some(String::from(c)); - } - } - KeyCode::Enter => { - if let Some(search) = &self.filter_search_input { - self.filters.push((search.to_string(), true)); - } - self.filter_search_input = None; - self.filter_search_mode = false; - } - KeyCode::Backspace => { - if let Some(search) = self.filter_search_input.as_mut() { - search.pop(); - if search.is_empty() { - self.filter_search_input = None; - } - } - } - _ => {} - } - return Ok(false); - } - } - match input { - Event::Key(key) if key.code == KeyCode::Up && key.kind == KeyEventKind::Press => { - // Select filter list item if filter is showing, otherwise scroll console. - if self.show_filter_menu { - self.selected_filter_index = self.selected_filter_index.saturating_sub(1); - } else { - // Scroll up - let mut scroll_speed = SCROLL_SPEED; - if key.modifiers.contains(SCROLL_MODIFIER_KEY) { - scroll_speed += SCROLL_MODIFIER; - } - self.scroll_position = self.scroll_position.saturating_sub(scroll_speed); - } + Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_keypress(key), + _ => Ok(Some(ServeUpdate::Redraw)), + } + } + + fn handle_keypress(&mut self, key: KeyEvent) -> io::Result> { + match key.code { + KeyCode::Char('r') => return Ok(Some(ServeUpdate::RequestRebuild)), + KeyCode::Char('o') => return Ok(Some(ServeUpdate::OpenApp)), + KeyCode::Char('p') => return Ok(Some(ServeUpdate::ToggleShouldRebuild)), + KeyCode::Char('v') => { + self.verbose = !self.verbose; + tracing::info!( + "Verbose logging is now {}", + if self.verbose { "on" } else { "off" } + ); } - Event::Key(key) if key.code == KeyCode::Down && key.kind == KeyEventKind::Press => { - // Select filter list item if filter is showing, otherwise scroll console. - if self.show_filter_menu { - let list_len = self.filters.len(); - if self.selected_filter_index + 1 < list_len { - self.selected_filter_index += 1; - } - } else { - // Scroll down - let mut scroll_speed = SCROLL_SPEED; - if key.modifiers.contains(SCROLL_MODIFIER_KEY) { - scroll_speed += SCROLL_MODIFIER; - } - self.scroll_position += scroll_speed; - } - } - Event::Key(key) if key.code == KeyCode::Left && key.kind == KeyEventKind::Press => { - // Remove selected filter if filter menu is shown. - if self.show_filter_menu { - let index = self.selected_filter_index; - if self.filters.get(index).is_some() { - self.filters.remove(index); - } - } - } - Event::Key(key) if key.code == KeyCode::Right && key.kind == KeyEventKind::Press => { - // Toggle filter if filter menu is shown. - if self.show_filter_menu { - let index = self.selected_filter_index; - self.filters.reverse(); - if let Some(item) = self.filters.get_mut(index) { - item.1 = !item.1; - } - self.filters.reverse(); - } - } - Event::Key(key) if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press => { - // We only need to listen to the enter key when not in search mode - // as there is other logic that handles adding filters and disabling the mode. - if self.show_filter_menu { - self.filter_search_mode = !self.filter_search_mode; - } - } - Event::Key(key) - if key.code == KeyCode::Char('r') && key.kind == KeyEventKind::Press => - { - // Reload the app - return Ok(true); - } - Event::Key(key) - if key.code == KeyCode::Char('o') && key.kind == KeyEventKind::Press => - { - // Open the running app. - open::that(format!("http://{}:{}", self.addr.addr, self.addr.port))?; + KeyCode::Char('t') => { + self.trace = !self.trace; + tracing::info!("Tracing is now {}", if self.trace { "on" } else { "off" }); } - Event::Key(key) - if key.code == KeyCode::Char('f') && key.kind == KeyEventKind::Press => - { - // Show filter menu and enable filter selection mode. - if self.show_filter_menu { - // Reset inputs when filter menu is closed. - self.filter_search_mode = false; - self.filter_search_input = None; + KeyCode::Char('c') => { + stdout() + .execute(Clear(ClearType::All))? + .execute(Clear(ClearType::Purge))?; + _ = self.term.borrow_mut().as_mut().map(|t| t.clear()); + } + + // Toggle the more modal by swapping the the terminal with a new one + // This is a bit of a hack since crossterm doesn't technically support changing the + // size of an inline viewport. + KeyCode::Char('/') => { + if let Some(terminal) = self.term.borrow_mut().as_mut() { + // Toggle the more modal, which will change our current viewport height + self.more_modal_open = !self.more_modal_open; + + // Clear the terminal before resizing it, such that it doesn't tear + terminal.clear()?; + + // And then set the new viewport, which essentially mimics a resize + *terminal = Terminal::with_options( + CrosstermBackend::new(stdout()), + TerminalOptions { + viewport: Viewport::Inline(self.viewport_current_height()), + }, + )?; } - self.show_filter_menu = !self.show_filter_menu; - } - Event::Key(key) - if key.code == KeyCode::Char('/') && key.kind == KeyEventKind::Press => - { - // Toggle more modal - self.more_modal_open = !self.more_modal_open; - } - Event::Resize(_width, _height) => { - // nothing, it should take care of itself } + _ => {} } - if self.scroll_position > self.num_lines_wrapping.saturating_sub(self.console_height) { - self.scroll_to_bottom(); - } - - Ok(false) - } - - pub fn new_ws_message( - &mut self, - platform: TargetPlatform, - message: axum::extract::ws::Message, - ) { - // Deccode the message and push it to our logs. - if let axum::extract::ws::Message::Text(text) = message { - let msg = serde_json::from_str::(text.as_str()); - match msg { - Ok(ClientMsg::Log { level, messages }) => { - let level = match level.as_str() { - "trace" => Level::TRACE, - "debug" => Level::DEBUG, - "info" => Level::INFO, - "warn" => Level::WARN, - "error" => Level::ERROR, - _ => Level::INFO, - }; - - let content = messages.first().unwrap_or(&String::new()).clone(); - - // We don't care about logging the app's message so we directly push it instead of using tracing. - self.push_log(TraceMsg::new(TraceSrc::App(platform), level, content)); - } - Err(err) => { - tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {}", platform, err); - } - } - } - } - - fn is_snapped(&self) -> bool { - true - } - - pub fn scroll_to_bottom(&mut self) { - self.scroll_position = self.num_lines_wrapping.saturating_sub(self.console_height); + // Out of safety, we always redraw, since it's relatively cheap operation + Ok(Some(ServeUpdate::Redraw)) } + /// Push a TraceMsg to be printed on the next render pub fn push_log(&mut self, message: TraceMsg) { - self.messages.push(message); + self.pending_logs.push_front(message); + } - if self.is_snapped() { - self.scroll_to_bottom(); + pub fn push_cargo_log(&mut self, message: cargo_metadata::CompilerMessage) { + use cargo_metadata::diagnostic::DiagnosticLevel; + + if self.trace + || matches!( + message.message.level, + DiagnosticLevel::Error | DiagnosticLevel::FailureNote + ) + { + self.push_log(TraceMsg::cargo(message)); } } - pub fn new_build_progress(&mut self, platform: TargetPlatform, update: UpdateBuildProgress) { - self.build_progress - .current_builds - .entry(platform) - .or_default() - .update(update); - - if self.is_snapped() { - self.scroll_to_bottom(); - } + /// Add a message from stderr to the logs + /// This will queue the stderr message as a TraceMsg and print it on the next render + /// We'll use the `App` TraceSrc for the msg, and whatever level is provided + pub fn push_stdio(&mut self, platform: Platform, msg: String, level: Level) { + self.push_log(TraceMsg::text(TraceSrc::App(platform), level, msg)); } - pub fn new_ready_app(&mut self, build_engine: &mut Builder, results: Vec) { - for result in results { - let out = build_engine - .children - .iter_mut() - .find_map(|(platform, child)| { - if platform == &result.target_platform { - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - Some((stdout, stderr)) - } else { - None - } - }); + /// Push a message from the websocket to the logs + pub fn push_ws_message(&mut self, platform: Platform, message: axum::extract::ws::Message) { + use dioxus_devtools_types::ClientMsg; - let platform = result.target_platform; + // We can only handle text messages from the websocket... + let axum::extract::ws::Message::Text(text) = message else { + return; + }; - let stdout = out.map(|(stdout, stderr)| RunningAppOutput { - stdout: BufReader::new(stdout).lines(), - stderr: BufReader::new(stderr).lines(), - stdout_line: String::new(), - stderr_line: String::new(), - }); + // ...and then decode them into a ClientMsg + let res = serde_json::from_str::(text.as_str()); - let app = RunningApp { - result, - output: stdout, - }; - - self.running_apps.insert(platform, app); - - // Finish the build progress for the platform that just finished building - if let Some(build) = self.build_progress.current_builds.get_mut(&platform) { - build.stage = Stage::Finished; + // Client logs being errors aren't fatal, but we should still report them them + let ClientMsg::Log { level, messages } = match res { + Ok(msg) => msg, + Err(err) => { + tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {}", platform, err); + return; } + }; + + // FIXME(jon): why are we pulling only the first message here? + let content = messages.first().unwrap_or(&String::new()).clone(); + + let level = match level.as_str() { + "trace" => Level::TRACE, + "debug" => Level::DEBUG, + "info" => Level::INFO, + "warn" => Level::WARN, + "error" => Level::ERROR, + _ => Level::INFO, + }; + + // We don't care about logging the app's message so we directly push it instead of using tracing. + self.push_log(TraceMsg::text(TraceSrc::App(platform), level, content)); + } + + /// Change internal state based on the build engine's update + /// + /// We want to keep internal state as limited as possible, so currently we're only setting our + /// animation tick. We could, in theory, just leave animation running and have no internal state, + /// but that seems a bit wasteful. We might eventually change this to be more of a "requestAnimationFrame" + /// approach, but then we'd need to do that *everywhere* instead of simply performing a react-like + /// re-render when external state changes. Ratatui will diff the intermediate buffer, so we at least + /// we won't be drawing it. + pub(crate) fn new_build_update(&mut self, update: &BuildUpdate) { + match update { + BuildUpdate::CompilerMessage { .. } => {} + BuildUpdate::Progress { .. } => self.tick_animation = true, + BuildUpdate::BuildReady { .. } => self.tick_animation = false, + BuildUpdate::BuildFailed { .. } => self.tick_animation = false, } } + /// Render the current state of everything to the console screen pub fn render( &mut self, - _opts: &Serve, - _config: &DioxusCrate, - _build_engine: &Builder, - _server: &Server, - _watcher: &Watcher, + opts: &ServeArgs, + config: &DioxusCrate, + build_engine: &Builder, + server: &WebServer, + watcher: &Watcher, ) { - // just drain the build logs if !self.interactive { - self.drain_print_logs(); return; } - // Keep the animation track in terms of 100ms frames - the frame should be a number between 0 and 10 - // todo: we want to use this somehow to animate things... - let elapsed = self.anim_start.elapsed().as_millis() as f32; - let num_frames = elapsed / 100.0; - let _frame_step = (num_frames % 10.0) as usize; - - _ = self - .term - .clone() - .borrow_mut() - .as_mut() - .unwrap() - .draw(|frame| { - let mut layout = render::TuiLayout::new(frame.size(), self.show_filter_menu); - let (console_width, console_height) = layout.get_console_size(); - self.console_width = console_width; - self.console_height = console_height; - - // Render the decor first as some of it (such as backgrounds) may be rendered on top of. - layout.render_decor(frame, self.show_filter_menu); - - // Get only the enabled filters. - let mut enabled_filters = self.filters.clone(); - enabled_filters.retain(|f| f.1); - let enabled_filters = enabled_filters - .iter() - .map(|f| f.0.clone()) - .collect::>(); - - // Render console, we need the number of wrapping lines for scroll. - self.num_lines_wrapping = layout.render_console( - frame, - self.scroll_position, - &self.messages, - &enabled_filters, - ); - - if self.show_filter_menu { - layout.render_filter_menu( - frame, - &self.filters, - self.selected_filter_index, - self.filter_search_mode, - self.filter_search_input.as_ref(), - ); - } - - layout.render_status_bar( - frame, - self.platform, - &self.build_progress, - self.more_modal_open, - self.show_filter_menu, - &self._dx_version, - ); - - if self.more_modal_open { - layout.render_more_modal(frame); - } - - layout.render_current_scroll( - self.scroll_position, - self.num_lines_wrapping, - self.console_height, - frame, - ); - }); - } - - async fn handle_events(&mut self, event: Event) -> io::Result { - let mut events = vec![event]; - - // Collect all the events within the next 10ms in one stream - let collect_events = async { - loop { - let Some(Ok(next)) = self.events.as_mut().unwrap().next().await else { - break; - }; - events.push(next); - } - }; - tokio::select! { - _ = collect_events => {}, - _ = tokio::time::sleep(Duration::from_millis(10)) => {} - } - - // Debounce events within the same frame - let mut handled = HashSet::new(); - for event in events { - if !handled.contains(&event) { - if self.handle_input(event.clone())? { - // Restart the running app. - return Ok(true); - } - handled.insert(event); - } - } - - Ok(false) - } -} - -#[derive(Default, Debug, PartialEq)] -pub struct ActiveBuild { - pub stage: Stage, - pub progress: f64, - pub failed: Option, -} - -impl ActiveBuild { - fn update(&mut self, update: UpdateBuildProgress) { - match update.update { - UpdateStage::Start => { - // If we are already past the stage, don't roll back, but allow a fresh build to update. - if self.stage > update.stage && self.stage < Stage::Finished { - return; - } - self.stage = update.stage; - self.progress = 0.0; - self.failed = None; - } - UpdateStage::SetProgress(progress) => { - self.progress = progress; - } - UpdateStage::Failed(failed) => { - self.stage = Stage::Finished; - self.failed = Some(failed.clone()); - } - } - } - - pub fn make_spans(&self, area: Rect) -> Vec { - let mut spans = Vec::new(); - - let message = match self.stage { - Stage::Initializing => "Initializing...", - Stage::InstallingWasmTooling => "Configuring...", - Stage::Compiling => "Compiling...", - Stage::OptimizingWasm => "Optimizing...", - Stage::OptimizingAssets => "Copying Assets...", - Stage::Finished => "Build finished! 🎉 ", + // Get a handle to the terminal with a different lifetime so we can continue to call &self methods + let owned_term = self.term.clone(); + let mut term = owned_term.borrow_mut(); + let Some(term) = term.as_mut() else { + return; }; - let progress = format!(" {}%", (self.progress * 100.0) as u8); + // First, dequeue any logs that have built up from event handling + _ = self.drain_logs(term); - if area.width >= self.max_layout_size() { - match self.stage { - Stage::Finished => spans.push(Span::from(message).light_yellow()), - _ => spans.push(Span::from(message).light_yellow()), - } + // Then, draw the frame, passing along all the state of the TUI so we can render it properly + _ = term.draw(|frame| { + self.render_frame( + frame, + RenderState { + opts, + krate: config, + build_engine, + server, + watcher, + }, + ); + }); + } - if self.stage != Stage::Finished { - spans.push(Span::from(progress).white()); - } + fn render_frame(&self, frame: &mut Frame, state: RenderState) { + // Use the max size of the viewport, but shrunk to a sensible max width + let mut area = frame.area(); + area.width = area.width.clamp(0, VIEWPORT_MAX_WIDTH); + + let [_top, body, bottom] = Layout::vertical([ + Constraint::Length(1), + Constraint::Fill(1), + Constraint::Length(1), + ]) + .horizontal_margin(1) + .areas(area); + + self.render_borders(frame, area); + self.render_body(frame, body, state); + self.render_bottom_row(frame, bottom, state); + self.render_body_title(frame, _top, state); + } + + fn render_body_title(&self, frame: &mut Frame<'_>, area: Rect, _state: RenderState) { + frame.render_widget( + Line::from(vec![ + " ".dark_gray(), + match self.more_modal_open { + true => "/:more".light_yellow(), + false => "/:more".dark_gray(), + }, + " ".dark_gray(), + ]) + .right_aligned(), + area, + ); + } + + fn render_body(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) { + let [_title, body, more, _foot] = Layout::vertical([ + Constraint::Length(0), + Constraint::Length(VIEWPORT_HEIGHT_SMALL - 2), + Constraint::Fill(1), + Constraint::Length(0), + ]) + .horizontal_margin(1) + .areas(area); + + let [col1, col2] = Layout::horizontal([Constraint::Length(50), Constraint::Fill(1)]) + .horizontal_margin(1) + .areas(body); + + self.render_gauges(frame, col1, state); + self.render_stats(frame, col2, state); + + if self.more_modal_open { + self.render_more_modal(frame, more, state); + } + } + + fn render_gauges(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) { + let [gauge_area, _margin] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(3)]).areas(area); + + let [app_progress, second_progress, status_line]: [_; 3] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]) + .areas(gauge_area); + + self.render_single_gauge( + frame, + app_progress, + state.build_engine.compile_progress(), + "App: ", + state, + state.build_engine.compile_duration(), + ); + + if state.build_engine.request.build.fullstack { + self.render_single_gauge( + frame, + second_progress, + state.build_engine.server_compile_progress(), + "Server: ", + state, + state.build_engine.compile_duration(), + ); } else { - spans.push(Span::from(progress).white()); + self.render_single_gauge( + frame, + second_progress, + state.build_engine.bundle_progress(), + "Bundle: ", + state, + state.build_engine.bundle_duration(), + ); } - spans + let mut lines = vec!["Status: ".white()]; + match &state.build_engine.stage { + BuildStage::Initializing => lines.push("Initializing".yellow()), + BuildStage::Starting { .. } => lines.push("Starting build".yellow()), + BuildStage::InstallingTooling {} => lines.push("Installing tooling".yellow()), + BuildStage::Compiling { + current, + total, + krate, + .. + } => { + lines.push("Compiling ".yellow()); + lines.push(format!("{current}/{total} ").gray()); + lines.push(krate.as_str().dark_gray()) + } + BuildStage::OptimizingWasm {} => lines.push("Optimizing wasm".yellow()), + BuildStage::RunningBindgen {} => lines.push("Running wasm-bindgen".yellow()), + BuildStage::Bundling {} => lines.push("Bundling app".yellow()), + BuildStage::CopyingAssets { + current, + total, + path, + } => { + lines.push("Copying asset ".yellow()); + lines.push(format!("{current}/{total} ").gray()); + if let Some(name) = path.file_name().and_then(|f| f.to_str()) { + lines.push(name.dark_gray()) + } + } + BuildStage::Success => { + lines.push("Serving ".yellow()); + lines.push(state.krate.executable_name().white()); + lines.push(" 🚀 ".green()); + if let Some(comp_time) = state.build_engine.total_build_time() { + lines.push(format!("{:.1}s", comp_time.as_secs_f32()).dark_gray()); + } + } + BuildStage::Failed => lines.push("Failed".red()), + BuildStage::Aborted => lines.push("Aborted".red()), + BuildStage::Restarting => lines.push("Restarting".yellow()), + }; + + frame.render_widget(Line::from(lines), status_line); } - pub fn max_layout_size(&self) -> u16 { - let progress_size = 4; - let stage_size = self.stage.to_string().len() as u16; - let brace_size = 2; + fn render_single_gauge( + &self, + frame: &mut Frame<'_>, + area: Rect, + value: f64, + label: &str, + state: RenderState, + time_taken: Option, + ) { + let failed = state.build_engine.stage == BuildStage::Failed; + let value = if failed { 1.0 } else { value.clamp(0.0, 1.0) }; - progress_size + stage_size + brace_size + let [gauge_row, _, icon] = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(2), + Constraint::Length(10), + ]) + .areas(area); + + frame.render_widget( + LineGauge::default() + .filled_style(Style::default().fg(match value { + 1.0 if failed => Color::Red, + 1.0 => Color::Green, + _ => Color::Yellow, + })) + .unfilled_style(Style::default().fg(Color::DarkGray)) + .label(label.gray()) + .line_set(symbols::line::THICK) + .ratio(if !failed { value } else { 1.0 }), + gauge_row, + ); + + let [throbber_frame, time_frame] = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(3), Constraint::Fill(1)]) + .areas(icon); + + if value != 1.0 { + let throb = throbber_widgets_tui::Throbber::default() + .style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan)) + .throbber_style( + ratatui::style::Style::default() + .fg(ratatui::style::Color::White) + .add_modifier(ratatui::style::Modifier::BOLD), + ) + .throbber_set(throbber_widgets_tui::BLACK_CIRCLE) + .use_type(throbber_widgets_tui::WhichUse::Spin); + frame.render_stateful_widget(throb, throbber_frame, &mut self.throbber.borrow_mut()); + } else { + frame.render_widget( + Line::from(vec![if failed { + "❌ ".white() + } else { + "🎉 ".white() + }]) + .left_aligned(), + throbber_frame, + ); + } + + if let Some(time_taken) = time_taken { + if !failed { + frame.render_widget( + Line::from(vec![format!("{:.1}s", time_taken.as_secs_f32()).dark_gray()]) + .left_aligned(), + time_frame, + ); + } + } + } + + fn render_stats(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) { + let [current_platform, app_features, serve_address]: [_; 3] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]) + .areas(area); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + "Platform: ".gray(), + self.platform.expected_name().yellow(), + if state.opts.build_arguments.fullstack { + " + fullstack".yellow() + } else { + " ".dark_gray() + }, + ])) + .wrap(Wrap { trim: false }), + current_platform, + ); + + self.render_feature_list(frame, app_features, state); + + // todo(jon) should we write https ? + let address = match state.server.server_address() { + Some(address) => format!("http://{}", address).blue(), + None => "no server address".dark_gray(), + }; + + frame.render_widget_ref( + Paragraph::new(Line::from(vec![ + if self.platform == Platform::Web { + "Serving at: ".gray() + } else { + "ServerFns at: ".gray() + }, + address, + ])) + .wrap(Wrap { trim: false }), + serve_address, + ); + } + + fn render_feature_list(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) { + frame.render_widget( + Paragraph::new(Line::from({ + let mut lines = vec!["App features: ".gray(), "[".yellow()]; + + let feature_list: Vec = state.build_engine.request.all_target_features(); + let num_features = feature_list.len(); + + for (idx, feature) in feature_list.into_iter().enumerate() { + lines.push("\"".yellow()); + lines.push(feature.yellow()); + lines.push("\"".yellow()); + if idx != num_features - 1 { + lines.push(", ".dark_gray()); + } + } + + lines.push("]".yellow()); + + lines + })) + .wrap(Wrap { trim: false }), + area, + ); + } + + fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, _state: RenderState) { + let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Length(2)]) + .horizontal_margin(1) + .areas(area); + + let meta_list: [_; 5] = Layout::vertical([ + Constraint::Length(1), // spacing + Constraint::Length(1), // item 1 + Constraint::Length(1), // item 2 + Constraint::Length(1), // item 3 + Constraint::Length(1), // Spacing + ]) + .areas(top); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + "dx version: ".gray(), + self.dx_version.as_str().yellow(), + ])), + meta_list[1], + ); + frame.render_widget( + Paragraph::new(Line::from(vec![ + "rustc: ".gray(), + "1.79.9 (nightly)".yellow(), + ])), + meta_list[2], + ); + frame.render_widget( + Paragraph::new(Line::from(vec!["Hotreload: ".gray(), "rsx only".yellow()])), + meta_list[3], + ); + + let links_list: [_; 2] = + Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + "Read the docs: ".gray(), + "https://dioxuslabs.com/0.6/docs".blue(), + ])), + links_list[0], + ); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + "Video tutorials: ".gray(), + "https://youtube.com/dioxuslabs".blue(), + ])), + links_list[1], + ); + } + + /// Render the version number on the bottom right + fn render_bottom_row(&self, frame: &mut Frame, area: Rect, _state: RenderState) { + // Split the area into two chunks + let row = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).split(area); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + // "🧬 dx".dark_gray(), + // " ".dark_gray(), + // self.dx_version.as_str().dark_gray(), + // " ".dark_gray(), + ])) + .right_aligned(), + row[1], + ); + } + + /// Render borders around the terminal, forcing an inner clear while we're at it + fn render_borders(&self, frame: &mut Frame, area: Rect) { + frame.render_widget(ratatui::widgets::Clear, area); + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::DarkGray)), + area, + ); + } + + /// Print logs to the terminal as close to a regular "println!()" as possible. + /// + /// We don't want alternate screens or other terminal tricks because we want these logs to be as + /// close to real as possible. Once the log is printed, it is lost, so we need to be very careful + /// here to not print it incorrectly. + /// + /// This method works by printing lines at the top of the viewport frame, and then scrolling up + /// the viewport accordingly, such that our final call to "clear" will cause the terminal the viewport + /// to be comlpetely erased and rewritten. This is slower since we're going around ratatui's diff + /// logic, but it's the only way to do this that gives us "true println!" semantics. + /// + /// In the future, Ratatui's insert_before method will get scroll regions, which will make this logic + /// much simpler. In that future, we'll simply insert a line into the scrollregion which should automatically + /// force that portion of the terminal to scroll up. + /// + /// TODO(jon): we could look into implementing scroll regions ourselves, but I think insert_before will + /// land in a reasonable amount of time. + #[deny(clippy::manual_saturating_arithmetic)] + fn drain_logs( + &mut self, + terminal: &mut Terminal>, + ) -> io::Result<()> { + use unicode_segmentation::UnicodeSegmentation; + + let Some(log) = self.pending_logs.pop_back() else { + return Ok(()); + }; + + // Only show debug logs if verbose is enabled + if log.level == Level::DEBUG && !self.verbose { + return Ok(()); + } + + if log.level == Level::TRACE && !self.trace { + return Ok(()); + } + + // Grab out the size and location of the terminal and its viewport before we start messing with it + let frame_rect = terminal.get_frame().area(); + let term_size = terminal.size().unwrap(); + + // Render the log into an ansi string + // We're going to add some metadata to it like the timestamp and source and then dump it to the raw ansi sequences we need to send to crossterm + let output_sequence = tracemsg_to_ansi_string(log, term_size.width); + + // Get the lines of the output sequence and their overflow + let lines = output_sequence.lines().collect::>(); + let lines_printed = lines + .iter() + .map(|line| { + // Very important to strip ansi codes before counting graphemes - the ansi codes count as multiple graphemes! + let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count() as u16; + grapheme_count.max(1).div_ceil(term_size.width) + }) + .sum::(); + + // The viewport might be clipped, but the math still needs to work out. + let actual_vh_height = self.viewport_current_height().min(term_size.height); + + // We don't need to add any pushback if the frame is in the middle of the viewport + // We'll then add some pushback to ensure the log scrolls up above the viewport. + let max_scrollback = lines_printed.min(actual_vh_height.saturating_sub(1)); + + // Move the terminal's cursor down to the number of lines printed + let remaining_space = term_size + .height + .saturating_sub(frame_rect.y + frame_rect.height); + + // Calculate how many lines we need to push back + let to_push = max_scrollback.saturating_sub(remaining_space + 1); + + // Wipe the viewport clean so it doesn't tear + crossterm::queue!( + std::io::stdout(), + crossterm::cursor::MoveTo(0, frame_rect.y), + crossterm::terminal::Clear(ClearType::FromCursorDown), + )?; + + // The only reliable way we can force the terminal downards is through "insert_before" + // If we need to push the terminal down, we'll use this method with the number of lines + // Ratatui will handle this rest. + // FIXME(jon): eventually insert_before will get scroll regions, breaking this, but making the logic here simpler + if to_push == 0 { + terminal.insert_before(lines_printed.saturating_sub(1), |_| {})?; + } + + // Start printing the log by writing on top of the topmost line + for (idx, line) in lines.into_iter().enumerate() { + // Move the cursor to the correct line offset but don't go past the bottom of the terminal + let start = frame_rect.y + idx as u16; + let start = start.min(term_size.height - 1); + crossterm::queue!( + std::io::stdout(), + crossterm::cursor::MoveTo(0, start), + crossterm::style::Print(line), + crossterm::style::Print("\n"), + )?; + } + + // Scroll the terminal if we need to + for _ in 0..to_push { + crossterm::queue!( + std::io::stdout(), + crossterm::cursor::MoveTo(0, term_size.height - 1), + crossterm::style::Print("\n"), + )?; + } + + // Force a clear + // Might've been triggered by insert_before already, but supposedly double-queuing is fine + // since this isn't a "real" synchronous clear + terminal.clear()?; + + Ok(()) + } + + fn viewport_current_height(&self) -> u16 { + match self.more_modal_open { + true => VIEWPORT_HEIGHT_BIG, + false => VIEWPORT_HEIGHT_SMALL, + } } } -impl PartialOrd for ActiveBuild { - fn partial_cmp(&self, other: &Self) -> Option { - Some( - self.stage - .cmp(&other.stage) - .then(self.progress.partial_cmp(&other.progress).unwrap()), - ) - } -} +fn tracemsg_to_ansi_string(log: TraceMsg, term_width: u16) -> String { + let rendered = match log.content { + TraceContent::Cargo(msg) => msg.message.rendered.unwrap_or_default(), + TraceContent::Text(text) => text, + }; -fn set_fix_term_hook() { - let original_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - _ = disable_raw_mode(); - let mut stdout = stdout(); - _ = stdout.execute(LeaveAlternateScreen); - _ = stdout.execute(Show); - original_hook(info); - })); -} + // Create a paragraph widget using the log line itself + // From here on out, we want to work with the escaped ansi string and the "real lines" to be printed + // + // We make a special case for lines that look like frames (ie ==== or ---- or ------) and make them + // dark gray, just for readability. + // + // todo(jon): refactor this out to accept any widget, not just paragraphs + let paragraph = Paragraph::new({ + use ansi_to_tui::IntoText; + use chrono::Timelike; -/// clearing and writing a new line fixes the xtermjs scrollback issue -fn fix_xtermjs_scrollback() { - _ = crossterm::execute!(std::io::stdout(), Clear(ClearType::All)); - println!(); -} + let mut text = Text::default(); -// todo: re-enable -#[allow(unused)] -async fn rustc_version() -> String { - tokio::process::Command::new("rustc") - .arg("--version") - .output() - .await - .ok() - .map(|o| o.stdout) - .and_then(|o| { - let out = String::from_utf8(o).unwrap(); - out.split_ascii_whitespace().nth(1).map(|v| v.to_string()) - }) - .unwrap_or_else(|| "".to_string()) -} + for (idx, raw_line) in rendered.lines().enumerate() { + let line_as_text = raw_line.into_text().unwrap(); + let is_pretending_to_be_frame = raw_line + .chars() + .all(|c| c == '=' || c == '-' || c == ' ' || c == '─'); -pub struct RunningApp { - result: BuildResult, - output: Option, -} + for (subline_idx, line) in line_as_text.lines.into_iter().enumerate() { + let mut out_line = if idx == 0 && subline_idx == 0 { + let mut formatted_line = Line::default(); -struct RunningAppOutput { - stdout: Lines>, - stderr: Lines>, - stdout_line: String, - stderr_line: String, + formatted_line.push_span( + Span::raw(format!( + "{:02}:{:02}:{:02} ", + log.timestamp.hour(), + log.timestamp.minute(), + log.timestamp.second() + )) + .dark_gray(), + ); + formatted_line.push_span( + Span::raw(format!( + "[{src}] {padding}", + src = log.source, + padding = + " ".repeat(3usize.saturating_sub(log.source.to_string().len())) + )) + .style(match log.source { + TraceSrc::App(_platform) => Style::new().blue(), + TraceSrc::Dev => Style::new().magenta(), + TraceSrc::Build => Style::new().yellow(), + TraceSrc::Bundle => Style::new().magenta(), + TraceSrc::Cargo => Style::new().yellow(), + TraceSrc::Unknown => Style::new().gray(), + }), + ); + + for span in line.spans { + formatted_line.push_span(span); + } + + formatted_line + } else { + line + }; + + if is_pretending_to_be_frame { + out_line = out_line.dark_gray(); + } + + text.lines.push(out_line); + } + } + + text + }); + + // We want to get the escaped ansii string and then by dumping the paragraph as ascii codes (again) + // + // This is important because the line_count method on paragraph takes into account the width of these codes + // the 3000 clip width is to bound log lines to a reasonable memory usage + // We could consider reusing this buffer since it's a lot to allocate, but log printing is not the + // slowest thing in the world and allocating is pretty fast... + // + // After we've dumped the ascii out, we want to call "trim_end" which ensures we don't attempt + // to print extra characters as lines, since AnsiStringBuffer will in fact attempt to print empty + // cells as characters. That might not actually be important, but we want to shrink the buffer + // before printing it + let line_count = paragraph.line_count(term_width); + AnsiStringBuffer::new(3000, line_count as u16).render(¶graph) } diff --git a/packages/cli/src/serve/proxy.rs b/packages/cli/src/serve/proxy.rs index 98194620a..7a7e2ae65 100644 --- a/packages/cli/src/serve/proxy.rs +++ b/packages/cli/src/serve/proxy.rs @@ -56,13 +56,13 @@ impl ProxyClient { /// - the exact path of the proxy config's backend URL, e.g. /api /// - the exact path with a trailing slash, e.g. /api/ /// - any subpath of the backend URL, e.g. /api/foo/bar -pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { +pub(crate) fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { let url: Uri = proxy.backend.parse()?; let path = url.path().to_string(); let trimmed_path = path.trim_start_matches('/'); if trimmed_path.is_empty() { - return Err(crate::Error::ProxySetupError(format!( + return Err(crate::Error::ProxySetup(format!( "Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}", proxy.backend.trim_end_matches('/'), proxy.backend @@ -142,6 +142,9 @@ pub(crate) fn proxy_to( } let uri = req.uri().clone(); + + // retry with backoff + let res = client.send(req).await.map_err(handle_error); match res { @@ -150,6 +153,7 @@ pub(crate) fn proxy_to( if uri.path().starts_with("/assets") || uri.path().starts_with("/_dioxus") || uri.path().starts_with("/public") + || uri.path().starts_with("/wasm") { tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri); } else { @@ -284,7 +288,7 @@ mod test { }; let router = super::add_proxy(Router::new(), &config); match router.unwrap_err() { - crate::Error::ProxySetupError(e) => { + crate::Error::ProxySetup(e) => { assert_eq!( e, "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000" diff --git a/packages/cli/src/serve/render.rs b/packages/cli/src/serve/render.rs deleted file mode 100644 index f25dd17ff..000000000 --- a/packages/cli/src/serve/render.rs +++ /dev/null @@ -1,487 +0,0 @@ -use super::BuildProgress; -use crate::{config::Platform, TraceMsg, TraceSrc}; -use ansi_to_tui::IntoText as _; -use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style, Stylize}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Clear, List, ListState, Paragraph, Widget, Wrap}, - Frame, -}; -use regex::Regex; -use std::fmt::Write as _; -use std::rc::Rc; -use tracing::Level; - -pub struct TuiLayout { - /// The entire TUI body. - _body: Rc<[Rect]>, - /// The console where build logs are displayed. - console: Rc<[Rect]>, - // The filter drawer if the drawer is open. - filter_drawer: Option>, - - // The border that separates the console and info bars. - border_sep: Rect, - - // The status bar that displays build status, platform, versions, etc. - status_bar: Rc<[Rect]>, - - // Misc - filter_list_state: ListState, -} - -impl TuiLayout { - pub fn new(frame_size: Rect, filter_open: bool) -> Self { - // The full layout - let body = Layout::default() - .direction(Direction::Vertical) - .constraints([ - // Footer Status - Constraint::Length(1), - // Border Separator - Constraint::Length(1), - // Console - Constraint::Fill(1), - // Padding - Constraint::Length(1), - ]) - .split(frame_size); - - let mut console_constraints = vec![Constraint::Fill(1)]; - if filter_open { - console_constraints.push(Constraint::Length(1)); - console_constraints.push(Constraint::Length(25)); - } - - // Build the console, where logs go. - let console = Layout::default() - .direction(Direction::Horizontal) - .constraints(console_constraints) - .split(body[2]); - - let filter_drawer = match filter_open { - false => None, - true => Some( - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(1), - Constraint::Fill(1), - Constraint::Length(1), - ]) - .split(console[2]), - ), - }; - - // Build the status bar. - let status_bar = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Fill(1), Constraint::Fill(1)]) - .split(body[0]); - - // Specify borders - let border_sep_top = body[1]; - - Self { - _body: body, - console, - filter_drawer, - border_sep: border_sep_top, - status_bar, - filter_list_state: ListState::default(), - } - } - - /// Render all decorations. - pub fn render_decor(&self, frame: &mut Frame, filter_open: bool) { - frame.render_widget( - Block::new() - .borders(Borders::TOP) - .border_style(Style::new().white()), - self.border_sep, - ); - - if filter_open { - frame.render_widget( - Block::new() - .borders(Borders::LEFT) - .border_style(Style::new().white()), - self.console[1], - ); - } - } - - /// Render the console and it's logs, returning the number of lines required to render the entire log output. - pub fn render_console( - &self, - frame: &mut Frame, - scroll_position: u16, - messages: &[TraceMsg], - enabled_filters: &[String], - ) -> u16 { - const LEVEL_MAX: usize = "BUILD: ".len(); - - let mut out_text = Text::default(); - - // Assemble the messages - for msg in messages.iter() { - let mut sub_line_padding = 0; - - let text = msg.content.trim_end().into_text().unwrap_or_default(); - - for (idx, line) in text.lines.into_iter().enumerate() { - // Don't add any formatting for cargo messages. - let out_line = if msg.source != TraceSrc::Cargo { - if idx == 0 { - match msg.source { - TraceSrc::Dev => { - let mut spans = vec![Span::from(" DEV: ").light_magenta()]; - - for span in line.spans { - spans.push(span); - } - spans - } - TraceSrc::Build => { - let mut spans = vec![Span::from("BUILD: ").light_blue()]; - - for span in line.spans { - spans.push(span); - } - spans - } - _ => { - // Build level tag: `INFO:`` - // We don't subtract 1 here for `:` because we still want at least 1 padding. - let padding = - build_msg_padding(LEVEL_MAX - msg.level.to_string().len() - 2); - let level = format!("{padding}{}: ", msg.level); - sub_line_padding += level.len(); - - let level_span = Span::from(level); - let level_span = match msg.level { - Level::TRACE => level_span.black(), - Level::DEBUG => level_span.light_magenta(), - Level::INFO => level_span.light_green(), - Level::WARN => level_span.light_yellow(), - Level::ERROR => level_span.light_red(), - }; - - let mut out_line = vec![level_span]; - for span in line.spans { - out_line.push(span); - } - - out_line - } - } - } else { - // Not the first line. Append the padding and merge into list. - let padding = build_msg_padding(sub_line_padding); - - let mut out_line = vec![Span::from(padding)]; - for span in line.spans { - out_line.push(span); - } - out_line - } - } else { - line.spans - }; - - out_text.push_line(Line::from(out_line)); - } - } - - // Only show messages for filters that are enabled. - let mut included_line_ids = Vec::new(); - - for filter in enabled_filters { - let re = Regex::new(filter); - for (index, line) in out_text.lines.iter().enumerate() { - let line_str = line.to_string(); - match re { - Ok(ref re) => { - // sort by provided regex - if re.is_match(&line_str) { - included_line_ids.push(index); - } - } - Err(_) => { - // default to basic string storing - if line_str.contains(filter) { - included_line_ids.push(index); - } - } - } - } - } - - included_line_ids.sort_unstable(); - included_line_ids.dedup(); - - let out_lines = out_text.lines; - let mut out_text = Text::default(); - - if enabled_filters.is_empty() { - for line in out_lines { - out_text.push_line(line.clone()); - } - } else { - for id in included_line_ids { - if let Some(line) = out_lines.get(id) { - out_text.push_line(line.clone()); - } - } - } - - let (console_width, _console_height) = self.get_console_size(); - - let paragraph = Paragraph::new(out_text) - .left_aligned() - .wrap(Wrap { trim: false }); - - let num_lines_wrapping = paragraph.line_count(console_width) as u16; - - paragraph - .scroll((scroll_position, 0)) - .render(self.console[0], frame.buffer_mut()); - - num_lines_wrapping - } - - /// Render the status bar. - pub fn render_status_bar( - &self, - frame: &mut Frame, - _platform: Platform, - build_progress: &BuildProgress, - more_modal_open: bool, - filter_menu_open: bool, - dx_version: &str, - ) { - // left aligned text - let mut spans = vec![ - Span::from("🧬 dx").white(), - Span::from(" ").white(), - Span::from(dx_version).white(), - Span::from(" | ").dark_gray(), - ]; - - // If there is build progress, render the current status. - let is_build_progress = !build_progress.current_builds.is_empty(); - if is_build_progress { - // If the build failed, show a failed status. - // Otherwise, render current status. - let build_failed = build_progress - .current_builds - .values() - .any(|b| b.failed.is_some()); - - if build_failed { - spans.push(Span::from("Build failed ❌").red()); - } else { - // spans.push(Span::from("status: ").gray()); - let build = build_progress - .current_builds - .values() - .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap(); - spans.extend_from_slice(&build.make_spans(Rect::new( - 0, - 0, - build.max_layout_size(), - 1, - ))); - } - } - - // right aligned text - let more_span = Span::from("[/] more"); - let more_span = match more_modal_open { - true => more_span.light_yellow(), - false => more_span.gray(), - }; - - let filter_span = Span::from("[f] filter"); - let filter_span = match filter_menu_open { - true => filter_span.light_yellow(), - false => filter_span.gray(), - }; - - // Right-aligned text - let right_line = Line::from(vec![ - Span::from("[o] open").gray(), - Span::from(" | ").gray(), - Span::from("[r] rebuild").gray(), - Span::from(" | ").gray(), - filter_span, - Span::from(" | ").dark_gray(), - more_span, - ]); - - frame.render_widget( - Paragraph::new(Line::from(spans)).left_aligned(), - self.status_bar[0], - ); - - // Render the info - frame.render_widget( - Paragraph::new(right_line).right_aligned(), - self.status_bar[1], - ); - } - - /// Renders the "more" modal to show extra info/keybinds accessible via the more keybind. - pub fn render_more_modal(&self, frame: &mut Frame) { - let modal = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(5)]) - .split(self.console[0])[1]; - - frame.render_widget(Clear, modal); - frame.render_widget(Block::default().borders(Borders::ALL), modal); - - // Render under construction message - frame.render_widget( - Paragraph::new("Under construction, please check back at a later date!") - .alignment(Alignment::Center), - modal, - ); - } - - /// Render the filter drawer menu. - pub fn render_filter_menu( - &mut self, - frame: &mut Frame, - filters: &[(String, bool)], - selected_filter_index: usize, - search_mode: bool, - search_input: Option<&String>, - ) { - let Some(ref filter_drawer) = self.filter_drawer else { - return; - }; - - // Vertical layout - let container = Layout::default() - .constraints([ - Constraint::Length(4), - Constraint::Fill(1), - Constraint::Length(7), - ]) - .direction(Direction::Vertical) - .split(filter_drawer[1]); - - // Render the search section. - let top_area = Layout::default() - .constraints([ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - ]) - .direction(Direction::Vertical) - .split(container[0]); - - let search_title = Line::from("Search").gray(); - let search_input_block = Block::new().bg(Color::White); - - let search_text = match search_input { - Some(s) => s, - None => { - if search_mode { - "..." - } else { - "[enter] to type..." - } - } - }; - - let search_input = Paragraph::new(Line::from(search_text)) - .fg(Color::Black) - .block(search_input_block); - - frame.render_widget(search_title, top_area[1]); - frame.render_widget(search_input, top_area[2]); - - // Render the filters - let list_area = container[1]; - let mut list_items = Vec::new(); - - for (filter, enabled) in filters { - let filter = Span::from(filter); - let filter = match enabled { - true => filter.light_yellow(), - false => filter.dark_gray(), - }; - list_items.push(filter); - } - list_items.reverse(); - - let list = List::new(list_items).highlight_symbol("» "); - self.filter_list_state.select(Some(selected_filter_index)); - frame.render_stateful_widget(list, list_area, &mut self.filter_list_state); - - // Render the keybind list at the bottom. - let keybinds = container[2]; - let lines = vec![ - Line::from(""), - Line::from("[↑] Up").white(), - Line::from("[↓] Down").white(), - Line::from("[←] Remove").white(), - Line::from("[→] Toggle").white(), - Line::from("[enter] Type / Submit").white(), - ]; - let text = Text::from(lines); - frame.render_widget(text, keybinds); - } - - /// Returns the height of the console TUI area in number of lines. - pub fn get_console_size(&self) -> (u16, u16) { - (self.console[0].width, self.console[0].height) - } - - /// Render the current scroll position at the top right corner of the frame - pub(crate) fn render_current_scroll( - &self, - scroll_position: u16, - lines: u16, - console_height: u16, - frame: &mut Frame<'_>, - ) { - let mut row = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1)]) - .split(self.console[0])[0]; - - // Hack: shove upwards the text to overlap with the border so text selection doesn't accidentally capture the number - row.y -= 1; - - let max_scroll = lines.saturating_sub(console_height); - if max_scroll == 0 { - return; - } - - let remaining_lines = max_scroll.saturating_sub(scroll_position); - if remaining_lines != 0 { - let text = vec![Span::from(format!(" {remaining_lines}⬇ ")).dark_gray()]; - frame.render_widget( - Paragraph::new(Line::from(text)) - .alignment(Alignment::Right) - .block(Block::default()), - row, - ); - } - } -} - -/// Generate a string with a specified number of spaces. -fn build_msg_padding(padding_len: usize) -> String { - let mut padding = String::new(); - for _ in 0..padding_len { - _ = write!(padding, " "); - } - padding -} diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs new file mode 100644 index 000000000..350a10d80 --- /dev/null +++ b/packages/cli/src/serve/runner.rs @@ -0,0 +1,312 @@ +use super::{AppHandle, ServeUpdate, WebServer}; +use crate::{ + AppBundle, DioxusCrate, HotreloadFilemap, HotreloadResult, Platform, Result, TraceSrc, +}; +use dioxus_core::internal::TemplateGlobalKey; +use dioxus_devtools_types::HotReloadMsg; +use dioxus_html::HtmlCtx; +use futures_util::{future::OptionFuture, stream::FuturesUnordered}; +use ignore::gitignore::Gitignore; +use std::{ + collections::{HashMap, HashSet}, + net::SocketAddr, + path::PathBuf, +}; +use tokio_stream::StreamExt; + +pub(crate) struct AppRunner { + pub(crate) running: HashMap, + pub(crate) krate: DioxusCrate, + pub(crate) file_map: HotreloadFilemap, + pub(crate) ignore: Gitignore, + pub(crate) applied_hot_reload_message: HotReloadMsg, + pub(crate) builds_opened: usize, + pub(crate) should_full_rebuild: bool, +} + +impl AppRunner { + /// Create the AppRunner and then initialize the filemap with the crate directory. + pub(crate) fn start(krate: &DioxusCrate) -> Self { + let mut runner = Self { + running: Default::default(), + file_map: HotreloadFilemap::new(), + applied_hot_reload_message: Default::default(), + ignore: krate.workspace_gitignore(), + krate: krate.clone(), + builds_opened: 0, + should_full_rebuild: true, + }; + + // todo(jon): this might take a while so we should try and background it, or make it lazy somehow + // we could spawn a thread to search the FS and then when it returns we can fill the filemap + // in testing, if this hits a massive directory, it might take several seconds with no feedback. + for krate in krate.all_watched_crates() { + runner.fill_filemap(krate); + } + + runner + } + + pub(crate) async fn wait(&mut self) -> ServeUpdate { + // If there are no running apps, we can just return pending to avoid deadlocking + if self.running.is_empty() { + return futures_util::future::pending().await; + } + + self.running + .iter_mut() + .map(|(platform, handle)| async { + use ServeUpdate::*; + let platform = *platform; + tokio::select! { + Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => { + StdoutReceived { platform, msg } + }, + Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => { + StderrReceived { platform, msg } + }, + Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => { + match status { + Ok(status) => ProcessExited { status, platform }, + Err(_err) => todo!("handle error in process joining?"), + } + } + Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => { + StdoutReceived { platform: Platform::Server, msg } + }, + Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => { + StderrReceived { platform: Platform::Server, msg } + }, + Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => { + match status { + Ok(status) => ProcessExited { status, platform: Platform::Server }, + Err(_err) => todo!("handle error in process joining?"), + } + } + else => futures_util::future::pending().await + } + }) + .collect::>() + .next() + .await + .expect("Stream to pending if not empty") + } + + /// Finally "bundle" this app and return a handle to it + pub(crate) async fn open( + &mut self, + app: AppBundle, + devserver_ip: SocketAddr, + fullstack_address: Option, + should_open_web: bool, + ) -> Result<&AppHandle> { + let platform = app.build.build.platform(); + + // Drop the old handle + // todo(jon): we should instead be sending the kill signal rather than dropping the process + // This would allow a more graceful shutdown and fix bugs like desktop not retaining its size + self.kill(platform); + + // wait a tiny sec for the processes to die so we don't have fullstack servers on top of each other + // todo(jon): we should allow rebinding to the same port in fullstack itself + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // Start the new app before we kill the old one to give it a little bit of time + let mut handle = AppHandle::new(app).await?; + handle + .open( + devserver_ip, + fullstack_address, + self.builds_opened == 0 && should_open_web, + ) + .await?; + self.builds_opened += 1; + self.running.insert(platform, handle); + + Ok(self.running.get(&platform).unwrap()) + } + + pub(crate) fn kill(&mut self, platform: Platform) { + self.running.remove(&platform); + } + + /// Open an existing app bundle, if it exists + pub(crate) async fn open_existing(&self, devserver: &WebServer) { + if let Some(address) = devserver.server_address() { + let url = format!("http://{address}"); + tracing::debug!("opening url: {url}"); + _ = open::that(url); + } + } + + pub(crate) fn attempt_hot_reload( + &mut self, + modified_files: Vec, + ) -> Option { + // If we have any changes to the rust files, we need to update the file map + let mut templates = vec![]; + + // Prepare the hotreload message we need to send + let mut edited_rust_files = Vec::new(); + let mut assets = Vec::new(); + + for path in modified_files { + // for various assets that might be linked in, we just try to hotreloading them forcefully + // That is, unless they appear in an include! macro, in which case we need to a full rebuild.... + let Some(ext) = path.extension().and_then(|v| v.to_str()) else { + continue; + }; + + // If it's a rust file, we want to hotreload it using the filemap + if ext == "rs" { + edited_rust_files.push(path); + continue; + } + + // Otherwise, it might be an asset and we should look for it in all the running apps + for runner in self.running.values() { + if let Some(bundled_name) = runner.hotreload_bundled_asset(&path) { + // todo(jon): don't hardcode this here + let asset_relative = PathBuf::from("/assets/").join(bundled_name); + assets.push(asset_relative); + } + } + } + + // Multiple runners might have queued the same asset, so dedup them + assets.dedup(); + + // Process the rust files + for rust_file in edited_rust_files { + // Strip the prefix before sending it to the filemap + let Ok(path) = rust_file.strip_prefix(self.krate.workspace_dir()) else { + tracing::error!( + "Hotreloading file outside of the crate directory: {:?}", + rust_file + ); + continue; + }; + + // And grabout the contents + let contents = std::fs::read_to_string(&rust_file).unwrap(); + + match self.file_map.update_rsx::(path, contents) { + HotreloadResult::Rsx(new) => templates.extend(new), + + // The rust file may have failed to parse, but that is most likely + // because the user is in the middle of adding new code + // We just ignore the error and let Rust analyzer warn about the problem + HotreloadResult::Notreloadable => return None, + HotreloadResult::NotParseable => { + tracing::debug!(dx_src = ?TraceSrc::Dev, "Error hotreloading file - not parseable {rust_file:?}") + } + } + } + + let msg = HotReloadMsg { + templates, + assets, + unknown_files: vec![], + }; + + self.add_hot_reload_message(&msg); + + Some(msg) + } + + /// Get any hot reload changes that have been applied since the last full rebuild + pub(crate) fn applied_hot_reload_changes(&mut self) -> HotReloadMsg { + self.applied_hot_reload_message.clone() + } + + /// Clear the hot reload changes. This should be called any time a new build is starting + pub(crate) fn clear_hot_reload_changes(&mut self) { + self.applied_hot_reload_message = Default::default(); + } + + /// Store the hot reload changes for any future clients that connect + fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) { + let applied = &mut self.applied_hot_reload_message; + + // Merge the assets, unknown files, and templates + // We keep the newer change if there is both a old and new change + let mut templates: HashMap = std::mem::take(&mut applied.templates) + .into_iter() + .map(|template| (template.key.clone(), template)) + .collect(); + let mut assets: HashSet = + std::mem::take(&mut applied.assets).into_iter().collect(); + let mut unknown_files: HashSet = std::mem::take(&mut applied.unknown_files) + .into_iter() + .collect(); + for template in &msg.templates { + templates.insert(template.key.clone(), template.clone()); + } + assets.extend(msg.assets.iter().cloned()); + unknown_files.extend(msg.unknown_files.iter().cloned()); + applied.templates = templates.into_values().collect(); + applied.assets = assets.into_iter().collect(); + applied.unknown_files = unknown_files.into_iter().collect(); + } + + pub(crate) async fn client_connected(&mut self) { + for (platform, runner) in self.running.iter_mut() { + // Assign the runtime asset dir to the runner + if *platform == Platform::Ios { + // xcrun simctl get_app_container booted com.dioxuslabs + let res = tokio::process::Command::new("xcrun") + .arg("simctl") + .arg("get_app_container") + .arg("booted") + .arg("com.dioxuslabs") + .output() + .await; + + if let Ok(res) = res { + tracing::debug!("Using runtime asset dir: {:?}", res); + + if let Ok(out) = String::from_utf8(res.stdout) { + let out = out.trim(); + + tracing::debug!("Setting Runtime asset dir: {out:?}"); + runner.runtime_asst_dir = Some(PathBuf::from(out)); + } + } + } + } + } + + /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include. + /// + /// You can use the filter with something like a gitignore to only include files that are relevant to your project. + /// We'll walk the filesystem from the given path and recursively search for all files that match the filter. + /// + /// The filter function takes a path and returns true if the file should be included in the filemap. + /// Generally this will only be .rs files + /// + /// If a file couldn't be parsed, we don't fail. Instead, we save the error. + pub fn fill_filemap(&mut self, path: PathBuf) { + if self.ignore.matched(&path, path.is_dir()).is_ignore() { + return; + } + + // If the file is a .rs file, add it to the filemap + if path.extension().and_then(|s| s.to_str()) == Some("rs") { + if let Ok(contents) = std::fs::read_to_string(&path) { + if let Ok(path) = path.strip_prefix(self.krate.workspace_dir()) { + self.file_map.add_file(path.to_path_buf(), contents); + } + } + return; + } + + // If it's not, we'll try to read the directory + if path.is_dir() { + if let Ok(read_dir) = std::fs::read_dir(&path) { + for entry in read_dir.flatten() { + self.fill_filemap(entry.path()); + } + } + } + } +} diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index ee08dd0a8..6535bbc56 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -1,183 +1,183 @@ -use crate::config::{Platform, WebHttpsConfig}; -use crate::serve::{next_or_pending, Serve}; -use crate::{dioxus_crate::DioxusCrate, TraceSrc}; -use crate::{Error, Result}; -use axum::extract::{Request, State}; -use axum::middleware::{self, Next}; +use crate::{ + config::WebHttpsConfig, + serve::{ServeArgs, ServeUpdate}, + BuildStage, BuildUpdate, DioxusCrate, Platform, Result, TraceSrc, +}; +use anyhow::Context; use axum::{ body::Body, extract::{ ws::{Message, WebSocket}, - WebSocketUpgrade, + Request, State, WebSocketUpgrade, }, http::{ header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA}, Method, Response, StatusCode, }, + middleware::{self, Next}, response::IntoResponse, routing::{get, get_service}, Extension, Router, }; -use axum_server::tls_rustls::RustlsConfig; -use dioxus_devtools::{DevserverMsg, HotReloadMsg}; +use dioxus_devtools_types::{DevserverMsg, HotReloadMsg}; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::stream; -use futures_util::{stream::FuturesUnordered, StreamExt}; -use hyper::header::ACCEPT; +use futures_util::{ + future, + stream::{self, FuturesUnordered}, + StreamExt, +}; use hyper::HeaderMap; use serde::{Deserialize, Serialize}; -use std::net::TcpListener; -use std::path::Path; -use std::sync::Arc; -use std::sync::RwLock; use std::{ convert::Infallible, fs, io, - net::{IpAddr, SocketAddr}, - process::Command, + net::{IpAddr, SocketAddr, TcpListener}, + path::Path, + sync::Arc, + sync::RwLock, }; -use tokio::task::JoinHandle; -use tower::ServiceBuilder; use tower_http::{ - cors::{Any, CorsLayer}, + cors::Any, services::fs::{ServeDir, ServeFileSystemResponseBody}, ServiceBuilderExt, }; -pub enum ServerUpdate { - NewConnection, - Message(Message), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", content = "data")] -enum Status { - ClientInit { - application_name: String, - platform: String, - }, - Building { - progress: f64, - build_message: String, - }, - BuildError { - error: String, - }, - Ready, -} - -#[derive(Debug, Clone)] -struct SharedStatus(Arc>); - -impl SharedStatus { - fn new(status: Status) -> Self { - Self(Arc::new(RwLock::new(status))) - } - - fn set(&self, status: Status) { - *self.0.write().unwrap() = status; - } - - fn get(&self) -> Status { - self.0.read().unwrap().clone() - } -} - -pub(crate) struct Server { - pub hot_reload_sockets: Vec, - pub build_status_sockets: Vec, - pub ip: SocketAddr, - pub new_hot_reload_sockets: UnboundedReceiver, - pub new_build_status_sockets: UnboundedReceiver, - _server_task: JoinHandle>, - /// We proxy (not hot reloading) fullstack requests to this port - pub fullstack_port: Option, - +/// The webserver that serves statics assets (if fullstack isn't already doing that) and the websocket +/// communication layer that we use to send status updates and hotreloads to the client. +/// +/// todo(jon): we should merge the build status and hotreload sockets into just a "devtools" socket +/// which carries all the message types. This would make it easier for us to add more message types +/// and better tooling on the pages that we serve. +pub(crate) struct WebServer { + devserver_ip: IpAddr, + devserver_port: u16, + proxied_port: Option, + hot_reload_sockets: Vec, + build_status_sockets: Vec, + new_hot_reload_sockets: UnboundedReceiver, + new_build_status_sockets: UnboundedReceiver, build_status: SharedStatus, application_name: String, - platform: String, + platform: Platform, } -impl Server { - pub fn start(serve: &Serve, cfg: &DioxusCrate) -> Self { +impl WebServer { + /// Start the development server. + /// This will set up the default http server if there's no server specified (usually via fullstack). + /// + /// This will also start the websocket server that powers the devtools. If you want to communicate + /// with connected devtools clients, this is the place to do it. + pub(crate) fn start(krate: &DioxusCrate, args: &ServeArgs) -> Result { let (hot_reload_sockets_tx, hot_reload_sockets_rx) = futures_channel::mpsc::unbounded(); let (build_status_sockets_tx, build_status_sockets_rx) = futures_channel::mpsc::unbounded(); - let build_status = SharedStatus::new(Status::Building { - progress: 0.0, - build_message: "Starting the build...".to_string(), - }); + let devserver_ip = args.address.addr; + let devserver_port = args.address.port; + let devserver_address = SocketAddr::new(devserver_ip, devserver_port); - let addr = serve.server_arguments.address.address(); - let start_browser = serve.server_arguments.open.unwrap_or_default(); + // All servers will end up behind us (the devserver) but on a different port + // This is so we can serve a loading screen as well as devtools without anything particularly fancy + let proxied_port = args + .should_proxy_build() + .then(|| get_available_port(devserver_ip)) + .flatten(); - // If we're serving a fullstack app, we need to find a port to proxy to - let fullstack_port = if matches!( - serve.build_arguments.platform(), - Platform::Liveview | Platform::Fullstack - ) { - get_available_port(addr.ip()) - } else { - None - }; + let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_ip, port)); - let fullstack_address = fullstack_port.map(|port| SocketAddr::new(addr.ip(), port)); - - let router = setup_router( - serve, - cfg, + // Set up the router with some shared state that we'll update later to reflect the current state of the build + let build_status = SharedStatus::new_with_starting_build(); + let router = build_devserver_router( + args, + krate, hot_reload_sockets_tx, build_status_sockets_tx, - fullstack_address, + proxied_address, build_status.clone(), - ) - .unwrap(); + )?; - // Actually just start the server, cloning in a few bits of config - let web_config = cfg.dioxus_config.web.https.clone(); - let base_path = cfg.dioxus_config.web.app.base_path.clone(); - let platform = serve.platform(); - let _server_task = tokio::spawn(async move { - let web_config = web_config.clone(); - // HTTPS - // Before console info so it can stop if mkcert isn't installed or fails - // todo: this is the only async thing here - might be nice to - let rustls: Option = get_rustls(&web_config).await.unwrap(); + // Create the listener that we'll pass into the devserver, but save its IP here so + // we can display it to the user in the tui + let listener = std::net::TcpListener::bind(devserver_address).with_context(|| { + anyhow::anyhow!( + "Failed to bind server to: {devserver_address}, is there another devserver running?\nTo run multiple devservers, use the --port flag to specify a different port" + ) + })?; - // Open the browser - if start_browser && platform != Platform::Desktop { - open_browser(base_path, addr, rustls.is_some()); - } + // And finally, start the server mainloop + tokio::spawn(devserver_mainloop( + krate.config.web.https.clone(), + listener, + router, + )); - // Start the server with or without rustls - if let Some(rustls) = rustls { - axum_server::bind_rustls(addr, rustls) - .serve(router.into_make_service()) - .await? - } else { - // Create a TCP listener bound to the address - axum::serve( - tokio::net::TcpListener::bind(&addr).await?, - router.into_make_service(), - ) - .await? - } - - Ok(()) - }); - - Self { + Ok(Self { + build_status, + proxied_port, + devserver_ip, + devserver_port, hot_reload_sockets: Default::default(), build_status_sockets: Default::default(), new_hot_reload_sockets: hot_reload_sockets_rx, new_build_status_sockets: build_status_sockets_rx, - _server_task, - ip: addr, - fullstack_port, + application_name: krate.executable_name().to_string(), + platform: args.build_arguments.platform(), + }) + } - build_status, - application_name: cfg.dioxus_config.application.name.clone(), - platform: serve.build_arguments.platform().to_string(), + /// Wait for new clients to be connected and then save them + pub(crate) async fn wait(&mut self) -> ServeUpdate { + let mut new_hot_reload_socket = self.new_hot_reload_sockets.next(); + let mut new_build_status_socket = self.new_build_status_sockets.next(); + let mut new_message = self + .hot_reload_sockets + .iter_mut() + .enumerate() + .map(|(idx, socket)| async move { (idx, socket.next().await) }) + .collect::>(); + + tokio::select! { + new_hot_reload_socket = &mut new_hot_reload_socket => { + if let Some(new_socket) = new_hot_reload_socket { + drop(new_message); + self.hot_reload_sockets.push(new_socket); + return ServeUpdate::NewConnection; + } else { + panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); + } + } + new_build_status_socket = &mut new_build_status_socket => { + if let Some(mut new_socket) = new_build_status_socket { + drop(new_message); + + // Update the socket with project info and current build status + let project_info = SharedStatus::new(Status::ClientInit { application_name: self.application_name.clone(), platform: self.platform }); + if project_info.send_to(&mut new_socket).await.is_ok() { + _ = self.build_status.send_to(&mut new_socket).await; + self.build_status_sockets.push(new_socket); + } + return future::pending::().await; + } else { + panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); + } + } + Some((idx, message)) = new_message.next() => { + match message { + Some(Ok(message)) => return ServeUpdate::WsMessage(message), + _ => { + drop(new_message); + _ = self.hot_reload_sockets.remove(idx); + } + } + } + } + + future::pending().await + } + + pub(crate) async fn shutdown(&mut self) { + self.send_shutdown().await; + for socket in self.hot_reload_sockets.drain(..) { + _ = socket.close().await; } } @@ -186,10 +186,7 @@ impl Server { let mut i = 0; while i < self.build_status_sockets.len() { let socket = &mut self.build_status_sockets[i]; - if send_build_status_to(&self.build_status, socket) - .await - .is_err() - { + if self.build_status.send_to(socket).await.is_err() { self.build_status_sockets.remove(i); } else { i += 1; @@ -198,7 +195,7 @@ impl Server { } /// Sends a start build message to all clients. - pub async fn start_build(&mut self) { + pub(crate) async fn start_build(&mut self) { self.build_status.set(Status::Building { progress: 0.0, build_message: "Starting the build...".to_string(), @@ -207,19 +204,54 @@ impl Server { } /// Sends an updated build status to all clients. - pub async fn update_build_status(&mut self, progress: f64, build_message: String) { - if !matches!(self.build_status.get(), Status::Building { .. }) { - return; + pub(crate) async fn new_build_update(&mut self, update: &BuildUpdate) { + match update { + BuildUpdate::Progress { stage } => { + // Todo(miles): wire up more messages into the splash screen UI + match stage { + BuildStage::Success => {} + BuildStage::Failed => self.send_reload_failed().await, + BuildStage::Restarting => self.send_reload_start().await, + BuildStage::Initializing => {} + BuildStage::InstallingTooling {} => {} + BuildStage::Compiling { + current, + total, + krate, + .. + } => { + self.build_status.set(Status::Building { + progress: (*current as f64 / *total as f64).clamp(0.0, 1.0), + build_message: format!("{krate} compiling"), + }); + self.send_build_status().await; + } + BuildStage::OptimizingWasm {} => {} + BuildStage::Aborted => {} + BuildStage::CopyingAssets { .. } => {} + _ => {} + } + } + BuildUpdate::CompilerMessage { .. } => {} + BuildUpdate::BuildReady { .. } => {} + BuildUpdate::BuildFailed { err } => { + let error = err.to_string(); + self.build_status.set(Status::BuildError { + error: ansi_to_html::convert(&error).unwrap_or(error), + }); + self.send_build_status().await; + } } - self.build_status.set(Status::Building { - progress, - build_message, - }); - self.send_build_status().await; } /// Sends hot reloadable changes to all clients. - pub async fn send_hotreload(&mut self, reload: HotReloadMsg) { + pub(crate) async fn send_hotreload(&mut self, reload: HotReloadMsg) { + if reload.is_empty() { + return; + } + + tracing::debug!("Sending hotreload to clients {:?}", reload); + let msg = DevserverMsg::HotReload(reload); let msg = serde_json::to_string(&msg).unwrap(); @@ -235,80 +267,22 @@ impl Server { } } - /// Wait for new clients to be connected and then save them - pub async fn wait(&mut self) -> Option { - let mut new_hot_reload_socket = self.new_hot_reload_sockets.next(); - let mut new_build_status_socket = self.new_build_status_sockets.next(); - let mut new_message = self - .hot_reload_sockets - .iter_mut() - .enumerate() - .map(|(idx, socket)| async move { (idx, socket.next().await) }) - .collect::>(); - let next_new_message = next_or_pending(new_message.next()); - - tokio::select! { - new_hot_reload_socket = &mut new_hot_reload_socket => { - if let Some(new_socket) = new_hot_reload_socket { - drop(new_message); - self.hot_reload_sockets.push(new_socket); - return Some(ServerUpdate::NewConnection); - } else { - panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); - } - } - new_build_status_socket = &mut new_build_status_socket => { - if let Some(mut new_socket) = new_build_status_socket { - drop(new_message); - - // Update the socket with project info and current build status - let project_info = SharedStatus::new(Status::ClientInit { application_name: self.application_name.clone(), platform: self.platform.clone() }); - if send_build_status_to(&project_info, &mut new_socket).await.is_ok() { - _ = send_build_status_to(&self.build_status, &mut new_socket).await; - self.build_status_sockets.push(new_socket); - } - return None; - } else { - panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); - } - } - (idx, message) = next_new_message => { - match message { - Some(Ok(message)) => return Some(ServerUpdate::Message(message)), - _ => { - drop(new_message); - _ = self.hot_reload_sockets.remove(idx); - } - } - } - } - - None - } - - /// Converts a `cargo` error to HTML and sends it to clients. - pub async fn send_build_error(&mut self, error: Error) { - let error = error.to_string(); - self.build_status.set(Status::BuildError { - error: ansi_to_html::convert(&error).unwrap_or(error), - }); - self.send_build_status().await; - } - /// Tells all clients that a full rebuild has started. - pub async fn send_reload_start(&mut self) { + pub(crate) async fn send_reload_start(&mut self) { self.send_devserver_message(DevserverMsg::FullReloadStart) .await; } /// Tells all clients that a full rebuild has failed. - pub async fn send_reload_failed(&mut self) { + pub(crate) async fn send_reload_failed(&mut self) { self.send_devserver_message(DevserverMsg::FullReloadFailed) .await; } /// Tells all clients to reload if possible for new changes. - pub async fn send_reload_command(&mut self) { + pub(crate) async fn send_reload_command(&mut self) { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + self.build_status.set(Status::Ready); self.send_build_status().await; self.send_devserver_message(DevserverMsg::FullReloadCommand) @@ -316,7 +290,7 @@ impl Server { } /// Send a shutdown message to all connected clients. - pub async fn send_shutdown(&mut self) { + pub(crate) async fn send_shutdown(&mut self) { self.send_devserver_message(DevserverMsg::Shutdown).await; } @@ -329,18 +303,48 @@ impl Server { } } - pub async fn shutdown(&mut self) { - self.send_shutdown().await; - for socket in self.hot_reload_sockets.drain(..) { - _ = socket.close().await; - } + /// Get the address the devserver should run on + pub fn devserver_address(&self) -> SocketAddr { + SocketAddr::new(self.devserver_ip, self.devserver_port) } - /// Get the address the fullstack server should run on if we're serving a fullstack app - pub fn fullstack_address(&self) -> Option { - self.fullstack_port - .map(|port| SocketAddr::new(self.ip.ip(), port)) + // Get the address the server should run on if we're serving the user's server + pub fn proxied_server_address(&self) -> Option { + self.proxied_port + .map(|port| SocketAddr::new(self.devserver_ip, port)) } + + pub fn server_address(&self) -> Option { + match self.platform { + Platform::Web | Platform::Server => Some(self.devserver_address()), + _ => self.proxied_server_address(), + } + } +} + +async fn devserver_mainloop( + https_cfg: WebHttpsConfig, + listener: TcpListener, + router: Router, +) -> Result<()> { + // We have a native listener that we're going to give to tokio, so we need to make it non-blocking + let _ = listener.set_nonblocking(true); + + // If we're not using rustls, just use regular axum + if https_cfg.enabled != Some(true) { + axum::serve(listener.try_into().unwrap(), router.into_make_service()).await?; + return Ok(()); + } + + // If we're using rustls, we need to get the cert/key paths and then set up rustls + let (cert_path, key_path) = get_rustls(&https_cfg).await?; + let rustls = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path).await?; + + axum_server::from_tcp_rustls(listener, rustls) + .serve(router.into_make_service()) + .await?; + + Ok(()) } /// Sets up and returns a router @@ -350,45 +354,25 @@ impl Server { /// - Setting up the proxy to the endpoint specified in the config /// - Setting up the file serve service /// - Setting up the websocket endpoint for devtools -fn setup_router( - serve: &Serve, - config: &DioxusCrate, +fn build_devserver_router( + args: &ServeArgs, + krate: &DioxusCrate, hot_reload_sockets: UnboundedSender, build_status_sockets: UnboundedSender, fullstack_address: Option, build_status: SharedStatus, ) -> Result { let mut router = Router::new(); - let platform = serve.build_arguments.platform(); // Setup proxy for the endpoint specified in the config - for proxy_config in config.dioxus_config.web.proxy.iter() { + for proxy_config in krate.config.web.proxy.iter() { router = super::proxy::add_proxy(router, proxy_config)?; } - // server the dir if it's web, otherwise let the fullstack server itself handle it - match platform { - Platform::Web | Platform::StaticGeneration => { - // Route file service to output the .wasm and assets if this is a web build - let base_path = format!( - "/{}", - config - .dioxus_config - .web - .app - .base_path - .as_deref() - .unwrap_or_default() - .trim_matches('/') - ); - - router = router.nest_service(&base_path, build_serve_dir(serve, config, platform)); - } - Platform::Liveview | Platform::Fullstack => { - // For fullstack and static generation, forward all requests to the server - let address = fullstack_address.unwrap(); - - router = router.nest_service("/",super::proxy::proxy_to( + if args.should_proxy_build() { + // For fullstack, liveview, and server, forward all requests to the inner server + let address = fullstack_address.unwrap(); + router = router.nest_service("/",super::proxy::proxy_to( format!("http://{address}").parse().unwrap(), true, |error| { @@ -401,8 +385,22 @@ fn setup_router( .unwrap() }, )); - } - _ => {} + } else { + // Otherwise, just serve the dir ourselves + // Route file service to output the .wasm and assets if this is a web build + let base_path = format!( + "/{}", + krate + .config + .web + .app + .base_path + .as_deref() + .unwrap_or_default() + .trim_matches('/') + ); + + router = router.nest_service(&base_path, build_serve_dir(args, krate)); } // Setup middleware to intercept html requests if the build status is "Building" @@ -419,7 +417,7 @@ fn setup_router( "/", get( |ws: WebSocketUpgrade, ext: Extension>| async move { - tracing::info!("Incoming hotreload websocket request: {ws:?}"); + tracing::debug!("New devtool websocket connection"); ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) }) }, ), @@ -438,7 +436,7 @@ fn setup_router( // Setup cors router = router.layer( - CorsLayer::new() + tower_http::cors::CorsLayer::new() // allow `GET` and `POST` when accessing the resource .allow_methods([Method::GET, Method::POST]) // allow requests from any origin @@ -449,11 +447,9 @@ fn setup_router( Ok(router) } -fn build_serve_dir( - serve: &Serve, - cfg: &DioxusCrate, - platform: Platform, -) -> axum::routing::MethodRouter { +fn build_serve_dir(args: &ServeArgs, cfg: &DioxusCrate) -> axum::routing::MethodRouter { + use tower::ServiceBuilder; + static CORS_UNSAFE: (HeaderValue, HeaderValue) = ( HeaderValue::from_static("unsafe-none"), HeaderValue::from_static("unsafe-none"), @@ -464,17 +460,15 @@ fn build_serve_dir( HeaderValue::from_static("same-origin"), ); - let (coep, coop) = match serve.server_arguments.cross_origin_policy { + let (coep, coop) = match args.cross_origin_policy { true => CORS_REQUIRE.clone(), false => CORS_UNSAFE.clone(), }; - let out_dir = match platform { - // Static generation only serves files from the public directory - Platform::StaticGeneration => cfg.out_dir().join("public"), - _ => cfg.out_dir(), - }; - let index_on_404 = cfg.dioxus_config.web.watcher.index_on_404; + let out_dir = cfg + .build_dir(Platform::Web, args.build_arguments.release) + .join("public"); + let index_on_404 = cfg.config.web.watcher.index_on_404; get_service( ServiceBuilder::new() @@ -508,18 +502,23 @@ fn no_cache( // If there's a 404 and we're supposed to index on 404, upgrade that failed request to the index.html // We might want to isnert a header here saying we *did* that but oh well if response.status() == StatusCode::NOT_FOUND && index_on_404 { - // First try to find a 404.html or 404/index.html file - let out_dir_404_html = out_dir.join("404.html"); - let out_dir_404_index_html = out_dir.join("404").join("index.html"); - let path = if out_dir_404_html.exists() { - out_dir_404_html - } else if out_dir_404_index_html.exists() { - out_dir_404_index_html - } else { - // If we can't find a 404.html or 404/index.html, just use the index.html - out_dir.join("index.html") - }; - let body = Body::from(std::fs::read_to_string(path).unwrap()); + let fallback = out_dir.join("index.html"); + let contents = std::fs::read_to_string(fallback).unwrap_or_else(|_| { + String::from( + r#" + + + + Err 404 - dx is not serving a web app + + +

Err 404 - dioxus is not currently serving a web app

+ + + "#, + ) + }); + let body = Body::from(contents); response = Response::builder() .status(StatusCode::OK) @@ -528,32 +527,28 @@ fn no_cache( }; insert_no_cache_headers(response.headers_mut()); + response } -pub fn insert_no_cache_headers(headers: &mut HeaderMap) { +pub(crate) fn insert_no_cache_headers(headers: &mut HeaderMap) { headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-cache")); headers.insert(PRAGMA, HeaderValue::from_static("no-cache")); headers.insert(EXPIRES, HeaderValue::from_static("0")); } -/// Returns an enum of rustls config -pub async fn get_rustls(web_config: &WebHttpsConfig) -> Result> { - if web_config.enabled != Some(true) { - return Ok(None); +async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> { + // If we're not using mkcert, just use the cert/key paths given to use in the config + if !web_config.mkcert.unwrap_or(false) { + if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) + { + return Ok((cert, key)); + } else { + // missing cert or key + return Err("https is enabled but cert or key path is missing".into()); + } } - let (cert_path, key_path) = match web_config.mkcert { - Some(true) => get_rustls_with_mkcert(web_config)?, - _ => get_rustls_without_mkcert(web_config)?, - }; - - Ok(Some( - RustlsConfig::from_pem_file(cert_path, key_path).await?, - )) -} - -pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> { const DEFAULT_KEY_PATH: &str = "ssl/key.pem"; const DEFAULT_CERT_PATH: &str = "ssl/cert.pem"; @@ -573,7 +568,7 @@ pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, St _ = fs::create_dir("ssl"); } - let cmd = Command::new("mkcert") + let cmd = tokio::process::Command::new("mkcert") .args([ "-install", "-key-file", @@ -599,33 +594,18 @@ pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, St return Err("failed to generate mkcert certificates".into()); } Ok(mut cmd) => { - cmd.wait()?; + cmd.wait().await?; } } Ok((cert_path, key_path)) } - -pub fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> { - // get paths to cert & key - if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) { - Ok((cert, key)) - } else { - // missing cert or key - Err("https is enabled but cert or key path is missing".into()) - } -} - -/// Open the browser to the address -pub(crate) fn open_browser(base_path: Option, address: SocketAddr, https: bool) { - let protocol = if https { "https" } else { "http" }; - let base_path = match base_path.as_deref() { - Some(base_path) => format!("/{}", base_path.trim_matches('/')), - None => "".to_owned(), - }; - _ = open::that(format!("{protocol}://{address}{base_path}")); -} - +/// Bind a listener to any point and return it +/// When the listener is dropped, the socket will be closed, but we'll still have a port that we +/// can bind our proxy to. +/// +/// Todo: we might want to do this on every new build in case the OS tries to bind things to this port +/// and we don't already have something bound to it. There's no great way of "reserving" a port. fn get_available_port(address: IpAddr) -> Option { TcpListener::bind((address, 0)) .map(|listener| listener.local_addr().unwrap().port()) @@ -639,7 +619,7 @@ async fn build_status_middleware( next: Next, ) -> axum::response::Response { // If the request is for html, and the status is "Building", return the loading page instead of the contents of the response - let accepts = request.headers().get(ACCEPT); + let accepts = request.headers().get(hyper::header::ACCEPT); let accepts_html = accepts .and_then(|v| v.to_str().ok()) .map(|v| v.contains("text/html")); @@ -647,7 +627,7 @@ async fn build_status_middleware( if let Some(true) = accepts_html { let status = state.get(); if status != Status::Ready { - let html = include_str!("../../assets/loading.html"); + let html = include_str!("../../assets/web/loading.html"); return axum::response::Response::builder() .status(StatusCode::OK) // Load the html loader then keep loading forever @@ -663,10 +643,48 @@ async fn build_status_middleware( next.run(request).await } -async fn send_build_status_to( - build_status: &SharedStatus, - socket: &mut WebSocket, -) -> Result<(), axum::Error> { - let msg = serde_json::to_string(&build_status.get()).unwrap(); - socket.send(Message::Text(msg)).await +#[derive(Debug, Clone)] +struct SharedStatus(Arc>); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +enum Status { + ClientInit { + application_name: String, + platform: Platform, + }, + Building { + progress: f64, + build_message: String, + }, + BuildError { + error: String, + }, + Ready, +} + +impl SharedStatus { + fn new(status: Status) -> Self { + Self(Arc::new(RwLock::new(status))) + } + + fn new_with_starting_build() -> Self { + Self::new(Status::Building { + progress: 0.0, + build_message: "Starting the build...".to_string(), + }) + } + + fn set(&self, status: Status) { + *self.0.write().unwrap() = status; + } + + fn get(&self) -> Status { + self.0.read().unwrap().clone() + } + + async fn send_to(&self, socket: &mut WebSocket) -> Result<(), axum::Error> { + let msg = serde_json::to_string(&self.get()).unwrap(); + socket.send(Message::Text(msg)).await + } } diff --git a/packages/cli/src/serve/update.rs b/packages/cli/src/serve/update.rs new file mode 100644 index 000000000..49e094adc --- /dev/null +++ b/packages/cli/src/serve/update.rs @@ -0,0 +1,59 @@ +use crate::{BuildUpdate, Platform, TraceMsg}; +use axum::extract::ws::Message as WsMessage; +use std::{path::PathBuf, process::ExitStatus}; + +/// One fat enum to rule them all.... +/// +/// Thanks to libraries like winit for the inspiration +#[allow(clippy::large_enum_variant)] +pub(crate) enum ServeUpdate { + NewConnection, + WsMessage(WsMessage), + + /// A build update from the build engine + BuildUpdate(BuildUpdate), + + /// A running process has received a stdout. + /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. + /// + /// We will poll lines and any content in a 50ms interval + StdoutReceived { + platform: Platform, + msg: String, + }, + + /// A running process has received a stderr. + /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. + /// + /// We will poll lines and any content in a 50ms interval + StderrReceived { + platform: Platform, + msg: String, + }, + + ProcessExited { + platform: Platform, + status: ExitStatus, + }, + + FilesChanged { + files: Vec, + }, + + /// Open an existing app bundle, if it exists + OpenApp, + + RequestRebuild, + + ToggleShouldRebuild, + + Redraw, + + TracingLog { + log: TraceMsg, + }, + + Exit { + error: Option>, + }, +} diff --git a/packages/cli/src/serve/watcher.rs b/packages/cli/src/serve/watcher.rs index 0fb9ea4a6..534369085 100644 --- a/packages/cli/src/serve/watcher.rs +++ b/packages/cli/src/serve/watcher.rs @@ -1,172 +1,66 @@ -use std::collections::{HashMap, HashSet}; -use std::{fs, path::PathBuf, time::Duration}; - -use super::hot_reloading_file_map::HotreloadError; -use crate::serve::hot_reloading_file_map::FileMap; -use crate::TraceSrc; -use crate::{cli::serve::Serve, dioxus_crate::DioxusCrate}; -use dioxus_devtools::HotReloadMsg; -use dioxus_html::HtmlCtx; +use super::detect::is_wsl; +use super::update::ServeUpdate; +use crate::{cli::serve::ServeArgs, dioxus_crate::DioxusCrate}; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::StreamExt; use ignore::gitignore::Gitignore; use notify::{ event::{MetadataKind, ModifyKind}, - Config, EventKind, + Config, EventKind, RecursiveMode, Watcher as NotifyWatcher, }; +use std::{path::PathBuf, time::Duration}; /// This struct stores the file watcher and the filemap for the project. /// /// This is where we do workspace discovery and recursively listen for changes in Rust files and asset /// directories. -pub struct Watcher { - _tx: UnboundedSender, +pub(crate) struct Watcher { rx: UnboundedReceiver, - _last_update_time: i64, - _watcher: Box, - queued_events: Vec, - file_map: FileMap, ignore: Gitignore, - applied_hot_reload_message: Option, + krate: DioxusCrate, + _tx: UnboundedSender, + watcher: Box, } impl Watcher { - pub fn start(serve: &Serve, config: &DioxusCrate) -> Self { + pub(crate) fn start(krate: &DioxusCrate, serve: &ServeArgs) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); - // Extend the watch path to include: - // - the assets directory - this is so we can hotreload CSS and other assets by default - // - the Cargo.toml file - this is so we can hotreload the project if the user changes dependencies - // - the Dioxus.toml file - this is so we can hotreload the project if the user changes the Dioxus config - let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone(); - allow_watch_path.push(config.dioxus_config.application.asset_dir.clone()); - allow_watch_path.push("Cargo.toml".to_string().into()); - allow_watch_path.push("Dioxus.toml".to_string().into()); - allow_watch_path.dedup(); - - let crate_dir = config.crate_dir(); - let mut builder = ignore::gitignore::GitignoreBuilder::new(&crate_dir); - builder.add(crate_dir.join(".gitignore")); - - let out_dir = config.out_dir(); - let out_dir_str = out_dir.display().to_string(); - - let excluded_paths = vec![ - ".git", - ".github", - ".vscode", - "target", - "node_modules", - "dist", - &out_dir_str, - ]; - for path in excluded_paths { - builder - .add_line(None, path) - .expect("failed to add path to file excluder"); - } - let ignore = builder.build().unwrap(); - - // Build the event handler for notify. - let notify_event_handler = { - let tx = tx.clone(); - move |info: notify::Result| { - if let Ok(e) = info { - if is_allowed_notify_event(&e) { - _ = tx.unbounded_send(e); - } - } - } - }; - - // If we are in WSL, we must use Notify's poll watcher due to an event propagation issue. - let is_wsl = is_wsl(); - const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories."; - - // Create the file watcher. - let mut watcher: Box = match is_wsl { - true => { - let poll_interval = Duration::from_secs( - serve.server_arguments.wsl_file_poll_interval.unwrap_or(2) as u64, - ); - - Box::new( - notify::PollWatcher::new( - notify_event_handler, - Config::default().with_poll_interval(poll_interval), - ) - .expect(NOTIFY_ERROR_MSG), - ) - } - false => { - Box::new(notify::recommended_watcher(notify_event_handler).expect(NOTIFY_ERROR_MSG)) - } - }; - - // Watch the specified paths - // todo: make sure we don't double-watch paths if they're nested - for sub_path in allow_watch_path { - let path = &config.crate_dir().join(sub_path); - - // If the path is ignored, don't watch it - if ignore.matched(path, path.is_dir()).is_ignore() { - continue; - } - - let mode = notify::RecursiveMode::Recursive; - - if let Err(err) = watcher.watch(path, mode) { - tracing::warn!("Failed to watch path: {}", err); - } - } - - // Probe the entire project looking for our rsx calls - // Whenever we get an update from the file watcher, we'll try to hotreload against this file map - let file_map = FileMap::create_with_filter::(config.crate_dir(), |path| { - ignore.matched(path, path.is_dir()).is_ignore() - }) - .unwrap(); - - Self { + let mut watcher = Self { + watcher: create_notify_watcher(serve, tx.clone()), _tx: tx, + krate: krate.clone(), rx, - _watcher: watcher, - file_map, - ignore, - queued_events: Vec::new(), - _last_update_time: chrono::Local::now().timestamp(), - applied_hot_reload_message: None, - } + ignore: krate.workspace_gitignore(), + }; + + watcher.watch_filesystem(); + + watcher } - /// A cancel safe handle to the file watcher - /// - /// todo: this should be simpler logic? - pub async fn wait(&mut self) { - // Pull off any queued events in succession - while let Ok(Some(event)) = self.rx.try_next() { - self.queued_events.push(event); + /// Wait for changed files to be detected + pub(crate) async fn wait(&mut self) -> ServeUpdate { + // Wait for the next file to change + let mut changes: Vec<_> = self.rx.next().await.into_iter().collect(); + + // Dequeue in bulk if we can, we might've received a lot of events in one go + while let Some(event) = self.rx.try_next().ok().flatten() { + changes.push(event); } - if !self.queued_events.is_empty() { - return; - } - - // If there are no queued events, wait for the next event - if let Some(event) = self.rx.next().await { - self.queued_events.push(event); - } - } - - /// Deques changed files from the event queue, doing the proper intelligent filtering - pub fn dequeue_changed_files(&mut self, config: &DioxusCrate) -> Vec { + // Filter the changes let mut all_mods: Vec = vec![]; // Decompose the events into a list of all the files that have changed - for event in self.queued_events.drain(..) { - // We only care about certain events. - if !is_allowed_notify_event(&event) { - continue; + for event in changes.drain(..) { + // Make sure we add new folders to the watch list, provided they're not matched by the ignore list + // We'll only watch new folders that are found under the crate, and then update our watcher to watch them + // This unfortunately won't pick up new krates added "at a distance" - IE krates not within the workspace. + if let EventKind::Create(_create_kind) = event.kind { + // If it's a new folder, watch it + // If it's a new cargo.toml (ie dep on the fly), + // todo(jon) support new folders on the fly } for path in event.paths { @@ -174,18 +68,8 @@ impl Watcher { } } - let mut modified_files = vec![]; - - // For the non-rust files, we want to check if it's an asset file - // This would mean the asset lives somewhere under the /assets directory or is referenced by magnanis in the linker - // todo: mg integration here - let _asset_dir = config - .dioxus_config - .application - .asset_dir - .canonicalize() - .ok(); - + // Collect the files that have changed + let mut files = vec![]; for path in all_mods.iter() { if path.extension().is_none() { continue; @@ -201,209 +85,99 @@ impl Watcher { } } - // If the extension is a backup file, or a hidden file, ignore it completely (no rebuilds) - if is_backup_file(path.to_path_buf()) { - tracing::trace!("Ignoring backup file: {:?}", path); - continue; - } - // If the path is ignored, don't watch it if self.ignore.matched(path, path.is_dir()).is_ignore() { continue; } - modified_files.push(path.clone()); + files.push(path.clone()); } - modified_files + tracing::debug!("Files changed: {files:?}"); + + ServeUpdate::FilesChanged { files } } - pub fn attempt_hot_reload( - &mut self, - config: &DioxusCrate, - modified_files: Vec, - ) -> Option { - // If we have any changes to the rust files, we need to update the file map - let crate_dir = config.crate_dir(); - let mut templates = vec![]; + fn watch_filesystem(&mut self) { + // Watch the folders of the crates that we're interested in + for path in self.krate.watch_paths() { + tracing::debug!("Watching path {path:?}"); - // Prepare the hotreload message we need to send - let mut edited_rust_files = Vec::new(); - let mut assets = Vec::new(); - let mut unknown_files = Vec::new(); - - for path in modified_files { - // for various assets that might be linked in, we just try to hotreloading them forcefully - // That is, unless they appear in an include! macro, in which case we need to a full rebuild.... - let Some(ext) = path.extension().and_then(|v| v.to_str()) else { - continue; - }; - - match ext { - "rs" => edited_rust_files.push(path), - _ if path.starts_with("assets") => assets.push(path), - _ => unknown_files.push(path), + if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) { + handle_notify_error(err); } } - for rust_file in edited_rust_files { - match self.file_map.update_rsx::(&rust_file, &crate_dir) { - Ok(hotreloaded_templates) => { - templates.extend(hotreloaded_templates); - } - // If the file is not reloadable, we need to rebuild - Err(HotreloadError::Notreloadable) => return None, - // The rust file may have failed to parse, but that is most likely - // because the user is in the middle of adding new code - // We just ignore the error and let Rust analyzer warn about the problem - Err(HotreloadError::Parse) => {} - // Otherwise just log the error - Err(err) => { - tracing::error!(dx_src = ?TraceSrc::Dev, "Error hotreloading file {rust_file:?}: {err}") - } + // Also watch the crates themselves, but not recursively, such that we can pick up new folders + for krate in self.krate.all_watched_crates() { + tracing::debug!("Watching path {krate:?}"); + if let Err(err) = self.watcher.watch(&krate, RecursiveMode::NonRecursive) { + handle_notify_error(err); } } - let msg = HotReloadMsg { - templates, - assets, - unknown_files, + // Also watch the workspace dir, non recursively, such that we can pick up new folders there too + if let Err(err) = self + .watcher + .watch(&self.krate.workspace_dir(), RecursiveMode::NonRecursive) + { + handle_notify_error(err); + } + } +} + +fn handle_notify_error(err: notify::Error) { + tracing::debug!("Failed to watch path: {}", err); + match err.kind { + notify::ErrorKind::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { + tracing::error!("Failed to watch path: permission denied. {:?}", err.paths) + } + notify::ErrorKind::MaxFilesWatch => { + tracing::error!("Failed to set up file watcher: too many files to watch") + } + _ => {} + } +} + +fn create_notify_watcher( + serve: &ServeArgs, + tx: UnboundedSender, +) -> Box { + // Build the event handler for notify. + let handler = move |info: notify::Result| { + let Ok(event) = info else { + return; }; - self.add_hot_reload_message(&msg); + let is_allowed_notify_event = match event.kind { + EventKind::Modify(ModifyKind::Data(_)) => true, + EventKind::Modify(ModifyKind::Name(_)) => true, + // The primary modification event on WSL's poll watcher. + EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true, + // Catch-all for unknown event types. + EventKind::Modify(ModifyKind::Any) => false, + EventKind::Modify(ModifyKind::Metadata(_)) => false, + // Don't care about anything else. + EventKind::Create(_) => true, + EventKind::Remove(_) => true, + _ => false, + }; - Some(msg) - } - - /// Get any hot reload changes that have been applied since the last full rebuild - pub fn applied_hot_reload_changes(&mut self) -> Option { - self.applied_hot_reload_message.clone() - } - - /// Clear the hot reload changes. This should be called any time a new build is starting - pub fn clear_hot_reload_changes(&mut self) { - self.applied_hot_reload_message.take(); - } - - /// Store the hot reload changes for any future clients that connect - fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) { - match &mut self.applied_hot_reload_message { - Some(applied) => { - // Merge the assets, unknown files, and templates - // We keep the newer change if there is both a old and new change - let mut templates: HashMap = std::mem::take(&mut applied.templates) - .into_iter() - .map(|template| (template.location.clone(), template)) - .collect(); - let mut assets: HashSet = - std::mem::take(&mut applied.assets).into_iter().collect(); - let mut unknown_files: HashSet = - std::mem::take(&mut applied.unknown_files) - .into_iter() - .collect(); - for template in &msg.templates { - templates.insert(template.location.clone(), template.clone()); - } - assets.extend(msg.assets.iter().cloned()); - unknown_files.extend(msg.unknown_files.iter().cloned()); - applied.templates = templates.into_values().collect(); - applied.assets = assets.into_iter().collect(); - applied.unknown_files = unknown_files.into_iter().collect(); - } - None => { - self.applied_hot_reload_message = Some(msg.clone()); - } + if is_allowed_notify_event { + _ = tx.unbounded_send(event); } + }; + + const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories."; + + if !is_wsl() { + return Box::new(notify::recommended_watcher(handler).expect(NOTIFY_ERROR_MSG)); } - /// Ensure the changes we've received from the queue are actually legit changes to either assets or - /// rust code. We don't care about changes otherwise, unless we get a signal elsewhere to do a full rebuild - pub fn pending_changes(&mut self) -> bool { - !self.queued_events.is_empty() - } -} + let poll_interval = Duration::from_secs(serve.wsl_file_poll_interval.unwrap_or(2) as u64); -fn is_backup_file(path: PathBuf) -> bool { - // If there's a tilde at the end of the file, it's a backup file - if let Some(name) = path.file_name() { - if let Some(name) = name.to_str() { - if name.ends_with('~') { - return true; - } - } - } - - // if the file is hidden, it's a backup file - if let Some(name) = path.file_name() { - if let Some(name) = name.to_str() { - if name.starts_with('.') { - return true; - } - } - } - - false -} - -/// Tests if the provided [`notify::Event`] is something we listen to so we can avoid unescessary hot reloads. -fn is_allowed_notify_event(event: ¬ify::Event) -> bool { - match event.kind { - EventKind::Modify(ModifyKind::Data(_)) => true, - EventKind::Modify(ModifyKind::Name(_)) => true, - EventKind::Create(_) => true, - EventKind::Remove(_) => true, - // The primary modification event on WSL's poll watcher. - EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true, - // Catch-all for unknown event types. - EventKind::Modify(ModifyKind::Any) => true, - // Don't care about anything else. - _ => false, - } -} - -const WSL_1: &str = "/proc/sys/kernel/osrelease"; -const WSL_2: &str = "/proc/version"; -const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"]; - -/// Detects if `dx` is being ran in a WSL environment. -/// -/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files. -/// This may fail in the future as it isn't guaranteed by Microsoft. -/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364 -fn is_wsl() -> bool { - // Test 1st File - if let Ok(content) = fs::read_to_string(WSL_1) { - let lowercase = content.to_lowercase(); - for keyword in WSL_KEYWORDS { - if lowercase.contains(keyword) { - return true; - } - } - } - - // Test 2nd File - if let Ok(content) = fs::read_to_string(WSL_2) { - let lowercase = content.to_lowercase(); - for keyword in WSL_KEYWORDS { - if lowercase.contains(keyword) { - return true; - } - } - } - - false -} - -#[test] -fn test_is_backup_file() { - assert!(is_backup_file(PathBuf::from("examples/test.rs~"))); - assert!(is_backup_file(PathBuf::from("examples/.back"))); - assert!(is_backup_file(PathBuf::from("test.rs~"))); - assert!(is_backup_file(PathBuf::from(".back"))); - - assert!(!is_backup_file(PathBuf::from("val.rs"))); - assert!(!is_backup_file(PathBuf::from( - "/Users/jonkelley/Development/Tinkering/basic_05_example/src/lib.rs" - ))); - assert!(!is_backup_file(PathBuf::from("exmaples/val.rs"))); + Box::new( + notify::PollWatcher::new(handler, Config::default().with_poll_interval(poll_interval)) + .expect(NOTIFY_ERROR_MSG), + ) } diff --git a/packages/cli/src/settings.rs b/packages/cli/src/settings.rs index bc680dfed..e9f97dbd5 100644 --- a/packages/cli/src/settings.rs +++ b/packages/cli/src/settings.rs @@ -1,13 +1,8 @@ +use crate::{Result, TraceSrc}; use serde::{Deserialize, Serialize}; -use std::{ - fs, - io::{Error, ErrorKind}, - path::PathBuf, -}; +use std::{fs, path::PathBuf}; use tracing::{debug, error, warn}; -use crate::{CrateConfigError, TraceSrc}; - const GLOBAL_SETTINGS_FILE_NAME: &str = "dioxus/settings.toml"; /// Describes cli settings from project or global level. @@ -18,26 +13,26 @@ const GLOBAL_SETTINGS_FILE_NAME: &str = "dioxus/settings.toml"; /// /// This allows users to control the cli settings with ease. #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct CliSettings { +pub(crate) struct CliSettings { /// Describes whether hot reload should always be on. - pub always_hot_reload: Option, + pub(crate) always_hot_reload: Option, /// Describes whether the CLI should always open the browser for Web targets. - pub always_open_browser: Option, + pub(crate) always_open_browser: Option, /// Describes whether desktop apps in development will be pinned always-on-top. - pub always_on_top: Option, + pub(crate) always_on_top: Option, /// Describes the interval in seconds that the CLI should poll for file changes on WSL. #[serde(default = "default_wsl_file_poll_interval")] - pub wsl_file_poll_interval: Option, + pub(crate) wsl_file_poll_interval: Option, } impl CliSettings { /// Load the settings from the local, global, or default config in that order - pub fn load() -> Self { + pub(crate) fn load() -> Self { Self::from_global().unwrap_or_default() } /// Get the current settings structure from global. - pub fn from_global() -> Option { + pub(crate) fn from_global() -> Option { let Some(path) = dirs::data_local_dir() else { warn!("failed to get local data directory, some config keys may be missing"); return None; @@ -63,18 +58,15 @@ impl CliSettings { /// Save the current structure to the global settings toml. /// This does not save to project-level settings. - pub fn save(self) -> Result { + pub(crate) fn save(self) -> Result { let path = Self::get_settings_path().ok_or_else(|| { error!(dx_src = ?TraceSrc::Dev, "failed to get settings path"); - CrateConfigError::Io(Error::new( - ErrorKind::NotFound, - "failed to get settings path", - )) + anyhow::anyhow!("failed to get settings path") })?; let data = toml::to_string_pretty(&self).map_err(|e| { error!(dx_src = ?TraceSrc::Dev, ?self, "failed to parse config into toml"); - CrateConfigError::Io(Error::new(ErrorKind::Other, e.to_string())) + anyhow::anyhow!("failed to parse config into toml: {e}") })?; // Create the directory structure if it doesn't exist. @@ -86,21 +78,23 @@ impl CliSettings { ?path, "failed to create directories for settings file" ); - return Err(CrateConfigError::Io(e)); + return Err( + anyhow::anyhow!("failed to create directories for settings file: {e}").into(), + ); } // Write the data. let result = fs::write(&path, data.clone()); if let Err(e) = result { error!(?data, ?path, "failed to save global cli settings"); - return Err(CrateConfigError::Io(e)); + return Err(anyhow::anyhow!("failed to save global cli settings: {e}").into()); } Ok(self) } /// Get the path to the settings toml file. - pub fn get_settings_path() -> Option { + pub(crate) fn get_settings_path() -> Option { let Some(path) = dirs::data_local_dir() else { warn!("failed to get local data directory, some config keys may be missing"); return None; @@ -110,7 +104,7 @@ impl CliSettings { } /// Modify the settings toml file - pub fn modify_settings(with: impl FnOnce(&mut CliSettings)) -> Result<(), CrateConfigError> { + pub(crate) fn modify_settings(with: impl FnOnce(&mut CliSettings)) -> Result<()> { let mut settings = Self::load(); with(&mut settings); settings.save()?; diff --git a/packages/cli/src/tooling.rs b/packages/cli/src/tooling.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/cli/src/tooling.rs @@ -0,0 +1 @@ + diff --git a/packages/cli/src/tracer.rs b/packages/cli/src/tracer.rs index 5016d79d9..be2e121da 100644 --- a/packages/cli/src/tracer.rs +++ b/packages/cli/src/tracer.rs @@ -14,108 +14,106 @@ //! 3. Build CLI layer for routing tracing logs to the TUI. //! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode. -use crate::builder::TargetPlatform; -use console::strip_ansi_codes; +use crate::{serve::ServeUpdate, Platform as TargetPlatform}; +use cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage}; use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use std::fmt::Display; +use once_cell::sync::OnceCell; use std::{ collections::HashMap, env, - fmt::{Debug, Write as _}, + fmt::{Debug, Display, Write as _}, fs, io::{self, Write}, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Mutex, + Mutex, }, }; -use tracing::Level; -use tracing::{field::Visit, Subscriber}; -use tracing_subscriber::{ - filter::filter_fn, fmt::format, prelude::*, registry::LookupSpan, EnvFilter, Layer, -}; +use tracing::{field::Visit, Level, Subscriber}; +use tracing_subscriber::{fmt::format, prelude::*, registry::LookupSpan, EnvFilter, Layer}; const LOG_ENV: &str = "DIOXUS_LOG"; const LOG_FILE_NAME: &str = "dx.log"; const DX_SRC_FLAG: &str = "dx_src"; -const DX_NO_FMT_FLAG: &str = "dx_no_fmt"; pub fn log_path() -> PathBuf { let tmp_dir = std::env::temp_dir(); tmp_dir.join(LOG_FILE_NAME) } -/// Build tracing infrastructure. -pub fn build_tracing() -> CLILogControl { - let mut filter = EnvFilter::new("error,dx=info,dioxus-cli=info,manganis-cli-support=info"); - if env::var(LOG_ENV).is_ok() { - filter = EnvFilter::from_env(LOG_ENV); +static TUI_ENABLED: AtomicBool = AtomicBool::new(false); +static TUI_TX: OnceCell> = OnceCell::new(); + +pub(crate) struct TraceController { + pub(crate) tui_rx: UnboundedReceiver, +} + +impl TraceController { + /// Get a handle to the trace controller. + pub fn redirect() -> Self { + let (tui_tx, tui_rx) = unbounded(); + TUI_ENABLED.store(true, Ordering::SeqCst); + TUI_TX.set(tui_tx.clone()).unwrap(); + Self { tui_rx } } - // Log file - let log_path = log_path(); - _ = std::fs::write(&log_path, ""); - let file_append_layer = match FileAppendLayer::new(log_path) { - Ok(f) => Some(f), - Err(e) => { - tracing::error!(dx_src = ?TraceSrc::Dev, err = ?e, "failed to init log file"); - None + /// Wait for the internal logger to send a message + pub(crate) async fn wait(&mut self) -> ServeUpdate { + use futures_util::StreamExt; + let log = self.tui_rx.next().await.expect("tracer should never die"); + ServeUpdate::TracingLog { log } + } + + pub(crate) fn shutdown(&self) { + TUI_ENABLED.store(false, Ordering::SeqCst); + } + + /// Build tracing infrastructure. + pub fn initialize() { + let mut filter = + EnvFilter::new("error,dx=trace,dioxus-cli=debug,manganis-cli-support=debug"); + + if env::var(LOG_ENV).is_ok() { + filter = EnvFilter::from_env(LOG_ENV); } - }; - // Create writer controller and custom writer. - let (output_tx, output_rx) = unbounded(); - let output_enabled = Arc::new(AtomicBool::new(false)); - let writer_control = CLILogControl { - output_rx, - output_enabled: output_enabled.clone(), - }; - - // Build CLI layer - let cli_layer = CLILayer::new(output_enabled.clone(), output_tx); - - // Build fmt layer - let formatter = format::debug_fn(|writer, field, value| { - write!(writer, "{}", format_field(field.name(), value)) - }) - .delimited(" "); - - // Format subscriber - let fmt_writer = Mutex::new(FmtLogWriter::new(output_enabled)); - let fmt_layer = tracing_subscriber::fmt::layer() - .fmt_fields(formatter) - .with_writer(fmt_writer) - .without_time() - .with_filter(filter_fn(|meta| { - // Filter any logs with "dx_no_fmt" or is not user facing (no dx_src) - let mut fields = meta.fields().iter(); - let has_src_flag = fields.any(|f| f.name() == DX_SRC_FLAG); - - if !has_src_flag { - return false; + // Log file + let log_path = log_path(); + _ = std::fs::write(&log_path, ""); + let file_append_layer = match FileAppendLayer::new(log_path) { + Ok(f) => Some(f), + Err(e) => { + tracing::error!(dx_src = ?TraceSrc::Dev, err = ?e, "failed to init log file"); + None } + }; - let has_fmt_flag = fields.any(|f| f.name() == DX_NO_FMT_FLAG); - if has_fmt_flag { - return false; - } + // Build CLI layer + let cli_layer = CLILayer; - true - })); + // Build fmt layer + let fmt_layer = tracing_subscriber::fmt::layer() + .fmt_fields( + format::debug_fn(|writer, field, value| { + write!(writer, "{}", format_field(field.name(), value)) + }) + .delimited(" "), + ) + .with_writer(Mutex::new(FmtLogWriter {})) + .with_timer(tracing_subscriber::fmt::time::time()); - let sub = tracing_subscriber::registry() - .with(filter) - .with(file_append_layer) - .with(cli_layer) - .with(fmt_layer); + let sub = tracing_subscriber::registry() + .with(filter) + .with(file_append_layer) + .with(cli_layer) + .with(fmt_layer); - #[cfg(feature = "tokio-console")] - let sub = sub.with(console_subscriber::spawn()); + #[cfg(feature = "tokio-console")] + let sub = sub.with(console_subscriber::spawn()); - sub.init(); - - writer_control + sub.init(); + } } /// A logging layer that appends to a file. @@ -148,9 +146,7 @@ where let mut visitor = CollectVisitor::new(); event.record(&mut visitor); - let new_line = if visitor.source == TraceSrc::Cargo - || event.fields().any(|f| f.name() == DX_NO_FMT_FLAG) - { + let new_line = if visitor.source == TraceSrc::Cargo { visitor.message } else { let meta = event.metadata(); @@ -172,7 +168,7 @@ where }; // Append logs - let new_data = strip_ansi_codes(&new_line).to_string(); + let new_data = console::strip_ansi_codes(&new_line).to_string(); if let Ok(mut buf) = self.buffer.lock() { *buf += &new_data; @@ -183,22 +179,7 @@ where } /// This is our "subscriber" (layer) that records structured data for the tui output. -struct CLILayer { - internal_output_enabled: Arc, - output_tx: UnboundedSender, -} - -impl CLILayer { - pub fn new( - internal_output_enabled: Arc, - output_tx: UnboundedSender, - ) -> Self { - Self { - internal_output_enabled, - output_tx, - } - } -} +struct CLILayer; impl Layer for CLILayer where @@ -210,21 +191,13 @@ where event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>, ) { - // We only care about user-facing logs. - let has_src_flag = event.fields().any(|f| f.name() == DX_SRC_FLAG); - if !has_src_flag { - return; - } - let mut visitor = CollectVisitor::new(); event.record(&mut visitor); // If the TUI output is disabled we let fmt subscriber handle the logs // EXCEPT for cargo logs which we just print. - if !self.internal_output_enabled.load(Ordering::SeqCst) { - if visitor.source == TraceSrc::Cargo - || event.fields().any(|f| f.name() == DX_NO_FMT_FLAG) - { + if !TUI_ENABLED.load(Ordering::SeqCst) { + if visitor.source == TraceSrc::Cargo { println!("{}", visitor.message); } return; @@ -244,8 +217,10 @@ where visitor.source = TraceSrc::Dev; } - self.output_tx - .unbounded_send(TraceMsg::new(visitor.source, *level, final_msg)) + TUI_TX + .get() + .unwrap() + .unbounded_send(TraceMsg::text(visitor.source, *level, final_msg)) .unwrap(); } @@ -256,7 +231,6 @@ where struct CollectVisitor { message: String, source: TraceSrc, - dx_user_msg: bool, fields: HashMap, } @@ -265,7 +239,7 @@ impl CollectVisitor { Self { message: String::new(), source: TraceSrc::Unknown, - dx_user_msg: false, + fields: HashMap::new(), } } @@ -285,7 +259,6 @@ impl Visit for CollectVisitor { if name == DX_SRC_FLAG { self.source = TraceSrc::from(value_string); - self.dx_user_msg = true; return; } @@ -293,42 +266,15 @@ impl Visit for CollectVisitor { } } -// Contains the sync primitives to control the CLIWriter. -pub struct CLILogControl { - pub output_rx: UnboundedReceiver, - pub output_enabled: Arc, -} - -struct FmtLogWriter { - stdout: io::Stdout, - output_enabled: Arc, -} - -impl FmtLogWriter { - pub fn new(output_enabled: Arc) -> Self { - Self { - stdout: io::stdout(), - output_enabled, - } - } -} +struct FmtLogWriter {} impl Write for FmtLogWriter { fn write(&mut self, buf: &[u8]) -> io::Result { - // Handle selection between TUI or Terminal output. - if !self.output_enabled.load(Ordering::SeqCst) { - self.stdout.write(buf) - } else { - Ok(buf.len()) - } + Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { - if !self.output_enabled.load(Ordering::SeqCst) { - self.stdout.flush() - } else { - Ok(()) - } + Ok(()) } } @@ -336,8 +282,6 @@ impl Write for FmtLogWriter { fn format_field(field_name: &str, value: &dyn Debug) -> String { let mut out = String::new(); match field_name { - DX_SRC_FLAG => write!(out, ""), - DX_NO_FMT_FLAG => write!(out, ""), "message" => write!(out, "{:?}", value), _ => write!(out, "{}={:?}", field_name, value), } @@ -350,15 +294,44 @@ fn format_field(field_name: &str, value: &dyn Debug) -> String { pub struct TraceMsg { pub source: TraceSrc, pub level: Level, - pub content: String, + pub content: TraceContent, + pub timestamp: chrono::DateTime, +} + +#[derive(Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum TraceContent { + Cargo(CompilerMessage), + Text(String), } impl TraceMsg { - pub fn new(source: TraceSrc, level: Level, content: String) -> Self { + pub fn text(source: TraceSrc, level: Level, content: String) -> Self { Self { source, level, - content, + content: TraceContent::Text(content), + timestamp: chrono::Local::now(), + } + } + + /// Create a new trace message from a cargo compiler message + /// + /// All `cargo` messages are logged at the `TRACE` level since they get *very* noisy during development + pub fn cargo(content: CompilerMessage) -> Self { + Self { + level: match content.message.level { + DiagnosticLevel::Ice => Level::ERROR, + DiagnosticLevel::Error => Level::ERROR, + DiagnosticLevel::FailureNote => Level::ERROR, + DiagnosticLevel::Warning => Level::TRACE, + DiagnosticLevel::Note => Level::TRACE, + DiagnosticLevel::Help => Level::TRACE, + _ => Level::TRACE, + }, + timestamp: chrono::Local::now(), + source: TraceSrc::Cargo, + content: TraceContent::Cargo(content), } } } @@ -368,9 +341,8 @@ pub enum TraceSrc { App(TargetPlatform), Dev, Build, - /// Provides no formatting. + Bundle, Cargo, - /// Avoid using this Unknown, } @@ -385,12 +357,13 @@ impl From for TraceSrc { fn from(value: String) -> Self { match value.as_str() { "dev" => Self::Dev, - "build" => Self::Build, + "bld" => Self::Build, "cargo" => Self::Cargo, - "web" => Self::App(TargetPlatform::Web), - "desktop" => Self::App(TargetPlatform::Desktop), + "app" => Self::App(TargetPlatform::Web), + "windows" => Self::App(TargetPlatform::Windows), + "macos" => Self::App(TargetPlatform::MacOS), + "linux" => Self::App(TargetPlatform::Linux), "server" => Self::App(TargetPlatform::Server), - "liveview" => Self::App(TargetPlatform::Liveview), _ => Self::Unknown, } } @@ -401,14 +374,19 @@ impl Display for TraceSrc { match self { Self::App(platform) => match platform { TargetPlatform::Web => write!(f, "web"), - TargetPlatform::Desktop => write!(f, "desktop"), + TargetPlatform::MacOS => write!(f, "macos"), + TargetPlatform::Windows => write!(f, "windows"), + TargetPlatform::Linux => write!(f, "linux"), TargetPlatform::Server => write!(f, "server"), - TargetPlatform::Liveview => write!(f, "server"), + TargetPlatform::Ios => write!(f, "ios"), + TargetPlatform::Android => write!(f, "android"), + TargetPlatform::Liveview => write!(f, "liveview"), }, Self::Dev => write!(f, "dev"), Self::Build => write!(f, "build"), Self::Cargo => write!(f, "cargo"), Self::Unknown => write!(f, "n/a"), + Self::Bundle => write!(f, "bundle"), } } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 8bbd687b5..3befb2fe5 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -190,7 +190,7 @@ mod field_info { pub builder_attr: FieldBuilderAttr, } - impl<'a> FieldInfo<'a> { + impl FieldInfo<'_> { pub fn new( ordinal: usize, field: &syn::Field, @@ -1018,11 +1018,11 @@ Finally, call `.build()` to create the instance of `{name}`. Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics dioxus_core::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause { - fn push_attribute( + fn push_attribute( mut self, ____name: &'static str, ____ns: Option<&'static str>, - ____attr: impl dioxus_core::prelude::IntoAttributeValue, + ____attr: impl dioxus_core::prelude::IntoAttributeValue, ____volatile: bool ) -> Self { let ( #(#descructuring,)* ) = self.fields; diff --git a/packages/core-types/src/attributes.rs b/packages/core-types/src/attributes.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core-types/src/bundled.rs b/packages/core-types/src/bundled.rs new file mode 100644 index 000000000..e7967d9ad --- /dev/null +++ b/packages/core-types/src/bundled.rs @@ -0,0 +1,16 @@ +static mut BUNDLED: bool = false; +static mut CHECKED_YET: bool = false; + +pub fn is_bundled_app() -> bool { + // unsafe to implement a dumb cell without pulling in deps / mutexes + // we're okay with a race condition here since the bundled flag is always set to the same value + unsafe { + if !CHECKED_YET { + CHECKED_YET = true; + BUNDLED = std::env::var("DIOXUS_CLI_ENABLED").is_ok() + || !matches!(std::env::var("CARGO_MANIFEST_DIR"), Ok(path) if !path.is_empty()); + } + + BUNDLED + } +} diff --git a/packages/core-types/src/event.rs b/packages/core-types/src/event.rs new file mode 100644 index 000000000..c8a5ff68a --- /dev/null +++ b/packages/core-types/src/event.rs @@ -0,0 +1,203 @@ +use std::{any::Any, cell::RefCell, rc::Rc}; + +/// A wrapper around some generic data that handles the event's state +/// +/// +/// Prevent this event from continuing to bubble up the tree to parent elements. +/// +/// # Example +/// +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// rsx! { +/// button { +/// onclick: move |evt: Event| { +/// evt.stop_propagation(); +/// } +/// } +/// }; +/// ``` +pub struct Event { + /// The data associated with this event + pub data: Rc, + pub(crate) metadata: Rc>, +} + +#[derive(Clone, Copy)] +pub(crate) struct EventMetadata { + pub(crate) propagates: bool, + pub(crate) prevent_default: bool, +} + +impl Event { + /// Create a new event from the inner data + pub fn new(data: Rc, propagates: bool) -> Self { + Self { + data, + metadata: Rc::new(RefCell::new(EventMetadata { + propagates, + prevent_default: false, + })), + } + } +} + +impl Event { + /// Map the event data to a new type + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// rsx! { + /// button { + /// onclick: move |evt: MouseEvent| { + /// let data = evt.map(|data| data.client_coordinates()); + /// println!("{:?}", data.data()); + /// } + /// } + /// }; + /// ``` + pub fn map U>(&self, f: F) -> Event { + Event { + data: Rc::new(f(&self.data)), + metadata: self.metadata.clone(), + } + } + + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// # #[allow(deprecated)] + /// evt.cancel_bubble(); + /// } + /// } + /// }; + /// ``` + #[deprecated = "use stop_propagation instead"] + pub fn cancel_bubble(&self) { + self.metadata.borrow_mut().propagates = false; + } + + /// Check if the event propagates up the tree to parent elements + pub fn propagates(&self) -> bool { + self.metadata.borrow().propagates + } + + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.stop_propagation(); + /// } + /// } + /// }; + /// ``` + pub fn stop_propagation(&self) { + self.metadata.borrow_mut().propagates = false; + } + + /// Get a reference to the inner data from this event + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.data(); + /// async move { + /// println!("{:?}", data); + /// } + /// } + /// } + /// }; + /// ``` + pub fn data(&self) -> Rc { + self.data.clone() + } + + /// Prevent the default action of the event. + /// + /// # Example + /// + /// ```rust + /// # use dioxus::prelude::*; + /// fn App() -> Element { + /// rsx! { + /// a { + /// // You can prevent the default action of the event with `prevent_default` + /// onclick: move |event| { + /// event.prevent_default(); + /// }, + /// href: "https://dioxuslabs.com", + /// "don't go to the link" + /// } + /// } + /// } + /// ``` + /// + /// Note: This must be called synchronously when handling the event. Calling it after the event has been handled will have no effect. + /// + ///
+ /// + /// This method is not available on the LiveView renderer because LiveView handles all events over a websocket which cannot block. + /// + ///
+ #[track_caller] + pub fn prevent_default(&self) { + self.metadata.borrow_mut().prevent_default = true; + } + + /// Check if the default action of the event is enabled. + pub fn default_action_enabled(&self) -> bool { + !self.metadata.borrow().prevent_default + } +} + +impl Event { + /// Downcast this `Event`` to a concrete type + pub fn downcast(self) -> Option> { + let data = self.data.downcast::().ok()?; + Some(Event { + metadata: self.metadata.clone(), + data, + }) + } +} + +impl Clone for Event { + fn clone(&self) -> Self { + Self { + metadata: self.metadata.clone(), + data: self.data.clone(), + } + } +} + +impl std::ops::Deref for Event { + type Target = Rc; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl std::fmt::Debug for Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UiEvent") + .field("bubble_state", &self.propagates()) + .field("prevent_default", &!self.default_action_enabled()) + .field("data", &self.data) + .finish() + } +} diff --git a/packages/core-types/src/formatter.rs b/packages/core-types/src/formatter.rs new file mode 100644 index 000000000..a4df24372 --- /dev/null +++ b/packages/core-types/src/formatter.rs @@ -0,0 +1,14 @@ +use std::borrow::Cow; + +/// Take this type and format it into a Cow<'static, str> +/// +/// This trait exists so libraries like manganis can implement this type for asssets without depending +/// on dioxus-core, which can be heavy in proc macros and build scripts. +/// +/// We don't want a blanket impl for T: Display because that might conflict for the other integral data +/// types of AttributeValue +/// +/// Todo: we might be able to specialize without this just with Display. +pub trait DioxusFormattable { + fn format(&self) -> Cow<'static, str>; +} diff --git a/packages/core-types/src/lib.rs b/packages/core-types/src/lib.rs index 25fa11493..0aa2cfcf9 100644 --- a/packages/core-types/src/lib.rs +++ b/packages/core-types/src/lib.rs @@ -1,5 +1,9 @@ pub mod bubbles; +pub mod bundled; +pub mod formatter; pub mod hr_context; pub use bubbles::*; +pub use bundled::*; +pub use formatter::*; pub use hr_context::*; diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index ec35fda57..9ba11ad46 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -10,45 +10,37 @@ homepage = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react"] [dependencies] -rustc-hash = { workspace = true } +dioxus-core-types = { workspace = true } +const_format = { workspace = true } +futures-channel = { workspace = true } +generational-box = { workspace = true } longest-increasing-subsequence = { workspace = true } -futures-util = { workspace = true, default-features = false, features = [ - "alloc", - "std", -] } +rustc-hash = { workspace = true } +rustversion = { workspace = true } slab = { workspace = true } slotmap = { workspace = true } -futures-channel = { workspace = true } tracing = { workspace = true } -serde = {workspace = true, optional = true, features = ["derive"] } -generational-box = { workspace = true } -rustversion = { workspace = true } -const_format = { workspace = true } warnings = { workspace = true } -manganis = { workspace = true, default-features = false, optional = true } +futures-util = { workspace = true, default-features = false, features = ["alloc", "std"] } +serde = { workspace = true, optional = true, features = ["derive"] } [dev-dependencies] -tokio = { workspace = true, features = ["full"] } -tracing-fluent-assertions = "0.3.0" dioxus = { workspace = true } -dioxus-html = { workspace = true, features = ["serialize"] } -pretty_assertions = "1.3.0" -rand = { workspace = true } dioxus-ssr = { workspace = true } -reqwest = { workspace = true} +dioxus-html = { workspace = true, features = ["serialize"] } +tokio = { workspace = true, features = ["full"] } +rand = { workspace = true } +reqwest = { workspace = true } tracing-subscriber = { workspace = true } +tracing-fluent-assertions = "0.3.0" +pretty_assertions = "1.3.0" [dev-dependencies.web-sys] version = "0.3.56" -features = [ - "Document", - "HtmlElement", - "Window" -] +features = ["Document", "HtmlElement", "Window"] [features] serialize = ["dep:serde"] -manganis = ["dep:manganis"] [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/core/src/hotreload_utils.rs b/packages/core/src/hotreload_utils.rs index e94ce0e50..84ad37de6 100644 --- a/packages/core/src/hotreload_utils.rs +++ b/packages/core/src/hotreload_utils.rs @@ -313,10 +313,20 @@ impl DynamicValuePool { #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct HotReloadTemplateWithLocation { - pub location: String, + pub key: TemplateGlobalKey, pub template: HotReloadedTemplate, } +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct TemplateGlobalKey { + pub file: String, + pub line: usize, + pub column: usize, + pub index: usize, +} + type StaticTemplateArray = &'static [TemplateNode]; #[doc(hidden)] diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 57c439a5f..97c5b0d94 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -40,6 +40,7 @@ pub mod internal { DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral, HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute, + TemplateGlobalKey, }; #[doc(hidden)] diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index aec3c08ca..e7bafa182 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,3 +1,5 @@ +use dioxus_core_types::DioxusFormattable; + use crate::innerlude::VProps; use crate::prelude::RenderError; use crate::{any_props::BoxedAnyProps, innerlude::ScopeState}; @@ -722,9 +724,9 @@ impl Attribute { /// /// "Volatile" refers to whether or not Dioxus should always override the value. This helps prevent the UI in /// some renderers stay in sync with the VirtualDom's understanding of the world - pub fn new( + pub fn new( name: &'static str, - value: impl IntoAttributeValue, + value: impl IntoAttributeValue, namespace: Option<&'static str>, volatile: bool, ) -> Attribute { @@ -1000,7 +1002,7 @@ where } /// A value that can be converted into an attribute value -pub trait IntoAttributeValue { +pub trait IntoAttributeValue { /// Convert into an attribute value fn into_value(self) -> AttributeValue; } @@ -1078,21 +1080,24 @@ impl IntoAttributeValue for Option { } } -#[cfg(feature = "manganis")] -impl IntoAttributeValue for manganis::ImageAsset { +pub struct AnyFmtMarker; +impl IntoAttributeValue for T +where + T: DioxusFormattable, +{ fn into_value(self) -> AttributeValue { - AttributeValue::Text(self.path().to_string()) + AttributeValue::Text(self.format().to_string()) } } /// A trait for anything that has a dynamic list of attributes pub trait HasAttributes { /// Push an attribute onto the list of attributes - fn push_attribute( + fn push_attribute( self, name: &'static str, ns: Option<&'static str>, - attr: impl IntoAttributeValue, + attr: impl IntoAttributeValue, volatile: bool, ) -> Self; } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 6e1f992db..d97a492c2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -28,7 +28,7 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } tracing = { workspace = true } -wry = { version = "0.43.0", default-features = false, features = [ +wry = { workspace = true, default-features = false, features = [ "os-webview", "protocol", "drag-drop", @@ -52,14 +52,15 @@ dioxus-hooks = { workspace = true } futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" -tao = { version = "0.26.1", features = ["rwh_05"] } +tao = { workspace = true, features = ["rwh_05"] } +once_cell = { workspace = true } [target.'cfg(unix)'.dependencies] signal-hook = "0.3.17" [target.'cfg(target_os = "linux")'.dependencies] -wry = { version = "0.43.0", features = [ +wry = { workspace = true, features = [ "os-webview", "protocol", "drag-drop", diff --git a/packages/desktop/src/android_sync_lock.rs b/packages/desktop/src/android_sync_lock.rs new file mode 100644 index 000000000..776f7ad25 --- /dev/null +++ b/packages/desktop/src/android_sync_lock.rs @@ -0,0 +1,11 @@ +/// This is a hack to get around the fact that wry is currently not thread safe on android +/// +/// We want to acquire this mutex before doing anything with the virtualdom directly +pub fn android_runtime_lock() -> std::sync::MutexGuard<'static, ()> { + use once_cell::sync::OnceCell; + use std::sync::Mutex; + + static RUNTIME_LOCK: OnceCell> = OnceCell::new(); + + RUNTIME_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() +} diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 749cbcb8d..389bcc483 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -226,8 +226,9 @@ impl App { pub fn handle_initialize_msg(&mut self, id: WindowId) { let view = self.webviews.get_mut(&id).unwrap(); - view.dom - .rebuild(&mut *view.edits.wry_queue.mutation_state_mut()); + view.edits + .wry_queue + .with_mutation_state_mut(|f| view.dom.rebuild(f)); view.edits.wry_queue.send_edits(); diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index 79f6cb284..ca282a407 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,4 +1,8 @@ -use std::{cell::RefCell, collections::VecDeque, rc::Rc, task::Waker}; +use std::{ + collections::VecDeque, + sync::{Arc, RwLock}, + task::Waker, +}; use dioxus_interpreter_js::MutationState; @@ -6,7 +10,7 @@ use dioxus_interpreter_js::MutationState; /// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made. #[derive(Default, Clone)] pub(crate) struct WryQueue { - inner: Rc>, + inner: Arc>, } #[derive(Default)] @@ -23,7 +27,7 @@ pub(crate) struct WryQueueInner { impl WryQueue { pub fn handle_request(&self, responder: wry::RequestAsyncResponder) { - let mut myself = self.inner.borrow_mut(); + let mut myself = self.inner.write().unwrap(); if let Some(bytes) = myself.edit_queue.pop_back() { responder.respond(wry::http::Response::new(bytes)); } else { @@ -36,13 +40,17 @@ impl WryQueue { } } - pub fn mutation_state_mut(&self) -> std::cell::RefMut<'_, MutationState> { - std::cell::RefMut::map(self.inner.borrow_mut(), |myself| &mut myself.mutation_state) + pub fn with_mutation_state_mut( + &self, + f: impl FnOnce(&mut MutationState) -> O, + ) -> O { + let mut inner = self.inner.write().unwrap(); + f(&mut inner.mutation_state) } /// Send a list of mutations to the webview pub(crate) fn send_edits(&self) { - let mut myself = self.inner.borrow_mut(); + let mut myself = self.inner.write().unwrap(); let serialized_edits = myself.mutation_state.export_memory(); // There are pending edits that need to be applied to the webview before we run futures myself.edits_in_progress = true; @@ -54,13 +62,13 @@ impl WryQueue { } fn edits_in_progress(&self) -> bool { - self.inner.borrow().edits_in_progress + self.inner.read().unwrap().edits_in_progress } /// Wait until all pending edits have been rendered in the webview pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> { if self.edits_in_progress() { - let mut myself = self.inner.borrow_mut(); + let mut myself = self.inner.write().unwrap(); let waker = cx.waker(); myself.waiting_for_edits_flushed.push(waker.clone()); std::task::Poll::Pending diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 45926e3f1..f23d97acd 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -2,7 +2,7 @@ Dioxus app - + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f1882d50a..8e7a4303d 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,6 +4,7 @@ #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] +mod android_sync_lock; mod app; mod assets; mod config; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 2c159ef8a..b7768c848 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -2,12 +2,7 @@ use crate::document::NATIVE_EVAL_JS; use crate::{assets::*, webview::WebviewEdits}; use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use dioxus_interpreter_js::NATIVE_JS; -use serde::Deserialize; -use std::{ - path::{Component, Path, PathBuf}, - process::Command, - sync::OnceLock, -}; +use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, RequestAsyncResponder, Result, @@ -27,6 +22,96 @@ const EVENTS_PATH: &str = "dioxus://index.html/__events"; static DEFAULT_INDEX: &str = include_str!("./index.html"); +#[allow(clippy::too_many_arguments)] // just for now, should fix this eventually +/// Handle a request from the webview +/// +/// - Tries to stream edits if they're requested. +/// - If that doesn't match, tries a user provided asset handler +/// - If that doesn't match, tries to serve a file from the filesystem +pub(super) fn desktop_handler( + request: Request>, + asset_handlers: AssetHandlerRegistry, + responder: RequestAsyncResponder, + edit_state: &WebviewEdits, + custom_head: Option, + custom_index: Option, + root_name: &str, + headless: bool, +) { + // Try to serve the index file first + if let Some(index_bytes) = + index_request(&request, custom_head, custom_index, root_name, headless) + { + return responder.respond(index_bytes); + } + + // If the request is asking for edits (ie binary protocol streaming), do that + let trimmed_uri = request.uri().path().trim_matches('/'); + if trimmed_uri == "__edits" { + return edit_state.wry_queue.handle_request(responder); + } + + // If the request is asking for an event response, do that + if trimmed_uri == "__events" { + return edit_state.handle_event(request, responder); + } + + // todo: we want to move the custom assets onto a different protocol or something + if let Some(name) = request.uri().path().split('/').next() { + if asset_handlers.has_handler(name) { + let _name = name.to_string(); + return asset_handlers.handle_request(&_name, request, responder); + } + } + + match serve_asset(request) { + Ok(res) => responder.respond(res), + Err(_e) => responder.respond( + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(String::from("Failed to serve asset").into_bytes()) + .unwrap(), + ), + } +} + +fn serve_asset(request: Request>) -> Result>> { + // If the user provided a custom asset handler, then call it and return the response if the request was handled. + // The path is the first part of the URI, so we need to trim the leading slash. + let mut uri_path = PathBuf::from( + urlencoding::decode(request.uri().path()) + .expect("expected URL to be UTF-8 encoded") + .as_ref(), + ); + + // If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle + // This lets us handle both absolute and relative paths without being too "special" + // It just means that our macos bundle is a little "special" because we need to place an `assets` + // dir in the `Resources` dir. + // + // If there's no asset root, we use the cargo manifest dir as the root, or the current dir + if !uri_path.exists() || uri_path.starts_with("/assets/") { + let bundle_root = get_asset_root(); + let relative_path = uri_path.strip_prefix("/").unwrap(); + uri_path = bundle_root.join(relative_path); + println!("bundle root: {bundle_root:?}"); + println!("uri path: {uri_path:?}"); + } + + // If the asset exists, then we can serve it! + if uri_path.exists() { + let mime_type = get_mime_from_path(&uri_path); + return Ok(Response::builder() + .header("Content-Type", mime_type?) + .header("Access-Control-Allow-Origin", "*") + .body(std::fs::read(uri_path)?)?); + } + + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes())?) +} + /// Build the index.html file we use for bootstrapping a new app /// /// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code @@ -36,7 +121,7 @@ static DEFAULT_INDEX: &str = include_str!("./index.html"); /// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can /// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you /// *do* have native control over elements, but that still won't work with liveview. -pub(super) fn index_request( +fn index_request( request: &Request>, custom_head: Option, custom_index: Option, @@ -53,17 +138,8 @@ pub(super) fn index_request( // Insert a custom head if provided // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail - let head = match custom_head { - Some(mut head) => { - if let Some(assets_head) = assets_head() { - head.push_str(&assets_head); - } - Some(head) - } - None => assets_head(), - }; - if let Some(head) = head { + if let Some(head) = custom_head { index.insert_str(index.find("").expect("Head element to exist"), &head); } @@ -82,141 +158,6 @@ pub(super) fn index_request( .ok() } -fn assets_head() -> Option { - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - let assets_head_path = PathBuf::from("__assets_head.html"); - let head = resolve_resource(&assets_head_path); - match std::fs::read_to_string(&head) { - Ok(s) => Some(s), - Err(err) => { - tracing::warn!("Assets built with manganis cannot be preloaded (failed to read {head:?}). This warning may occur when you build a desktop application without the dioxus CLI. If you do not use manganis, you can ignore this warning: {err}."); - None - } - } - } - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - )))] - { - None - } -} - -fn resolve_resource(path: &Path) -> PathBuf { - let mut base_path = get_asset_root_or_default(); - - if running_in_dev_mode() { - base_path.push(path); - - // Special handler for Manganis filesystem fallback. - // We need this since Manganis provides assets from workspace root. - if !base_path.exists() { - let workspace_root = get_workspace_root_from_cargo(); - let asset_path = workspace_root.join(path); - return asset_path; - } - } else { - let mut resource_path = PathBuf::new(); - for component in path.components() { - // Tauri-bundle inserts special path segments for abnormal component paths - match component { - Component::Prefix(_) => {} - Component::RootDir => resource_path.push("_root_"), - Component::CurDir => {} - Component::ParentDir => resource_path.push("_up_"), - Component::Normal(p) => resource_path.push(p), - } - } - base_path.push(resource_path); - } - base_path -} - -/// Handle a request from the webview -/// -/// - Tries to stream edits if they're requested. -/// - If that doesn't match, tries a user provided asset handler -/// - If that doesn't match, tries to serve a file from the filesystem -pub(super) fn desktop_handler( - request: Request>, - asset_handlers: AssetHandlerRegistry, - responder: RequestAsyncResponder, - edit_state: &WebviewEdits, -) { - // If the request is asking for edits (ie binary protocol streaming), do that - let trimmed_uri = request.uri().path().trim_matches('/'); - if trimmed_uri == "__edits" { - return edit_state.wry_queue.handle_request(responder); - } - - // If the request is asking for an event response, do that - if trimmed_uri == "__events" { - return edit_state.handle_event(request, responder); - } - - // If the user provided a custom asset handler, then call it and return the response if the request was handled. - // The path is the first part of the URI, so we need to trim the leading slash. - let path = PathBuf::from( - urlencoding::decode(request.uri().path().trim_start_matches('/')) - .expect("expected URL to be UTF-8 encoded") - .as_ref(), - ); - - if path.parent().is_none() { - return tracing::error!("Asset request has no parent {path:?}"); - } - - if let Some(name) = path.iter().next().unwrap().to_str() { - if asset_handlers.has_handler(name) { - return asset_handlers.handle_request(name, request, responder); - } - } - - // Else, try to serve a file from the filesystem. - match serve_from_fs(path) { - Ok(res) => responder.respond(res), - Err(e) => { - tracing::error!("Error serving request from filesystem {}", e); - } - } -} - -fn serve_from_fs(path: PathBuf) -> Result>> { - // If the path is relative, we'll try to serve it from the assets directory. - let mut asset = resolve_resource(&path); - - // If we can't find it, make it absolute and try again - if !asset.exists() { - asset = PathBuf::from("/").join(&path); - } - - if !asset.exists() { - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes())?); - } - - Ok(Response::builder() - .header("Content-Type", get_mime_from_path(&asset)?) - .header("Access-Control-Allow-Origin", "*") - .body(std::fs::read(asset)?)?) -} - /// Construct the inline script that boots up the page and bridges the webview with rust code. /// /// The arguments here: @@ -254,109 +195,71 @@ fn module_loader(root_id: &str, headless: bool) -> String { ) } -/// Get the asset directory, following tauri/cargo-bundles directory discovery approach -/// -/// Defaults to the current directory if no asset directory is found, which is useful for development when the app -/// isn't bundled. -fn get_asset_root_or_default() -> PathBuf { - get_asset_root().unwrap_or_else(|| std::env::current_dir().unwrap()) -} - -fn running_in_dev_mode() -> bool { - // If running under cargo, there's no bundle! - // There might be a smarter/more resilient way of doing this - std::env::var_os("CARGO").is_some() -} - /// Get the asset directory, following tauri/cargo-bundles directory discovery approach /// /// Currently supports: /// - [x] macOS -/// - [ ] Windows +/// - [x] iOS +/// - [x] Windows +/// - [x] Linux (appimage) /// - [ ] Linux (rpm) /// - [ ] Linux (deb) -/// - [ ] iOS /// - [ ] Android #[allow(unreachable_code)] -fn get_asset_root() -> Option { - if running_in_dev_mode() { - // todo: we don't want to canonicalize assets like this, but it will take longer to migrate, so we'll do it later - // we should just be parsing paths the way they are instead of relative to the "asset" dir - // manganis will eventually just dump the raw path to us, but until then, we need to canonicalize here - return dioxus_cli_config::base_path(); - } +fn get_asset_root() -> PathBuf { + let cur_exe = std::env::current_exe().unwrap(); #[cfg(target_os = "macos")] { - let bundle = core_foundation::bundle::CFBundle::main_bundle(); - let bundle_path = bundle.path()?; - let resources_path = bundle.resources_path()?; - let absolute_resources_root = bundle_path.join(resources_path); - return dunce::canonicalize(absolute_resources_root).ok(); + return cur_exe + .parent() + .unwrap() + .parent() + .unwrap() + .join("Resources"); } - None + // For all others, the structure looks like this: + // app.(exe/appimage) + // main.exe + // assets/ + cur_exe.parent().unwrap().to_path_buf() } /// Get the mime type from a path-like string -fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { - if trimmed.extension().is_some_and(|ext| ext == "svg") { +fn get_mime_from_path(asset: &Path) -> Result<&'static str> { + if asset.extension().is_some_and(|ext| ext == "svg") { return Ok("image/svg+xml"); } - match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { + match infer::get_from_path(asset)?.map(|f| f.mime_type()) { Some(f) if f != "text/plain" => Ok(f), - _ => Ok(get_mime_by_ext(trimmed)), + _other => Ok(get_mime_by_ext(asset)), } } /// Get the mime type from a URI using its extension fn get_mime_by_ext(trimmed: &Path) -> &'static str { match trimmed.extension().and_then(|e| e.to_str()) { + // The common assets are all utf-8 encoded + Some("js") => "text/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("json") => "application/json; charset=utf-8", + Some("svg") => "image/svg+xml; charset=utf-8", + Some("html") => "text/html; charset=utf-8", + + // the rest... idk? probably not + Some("mjs") => "text/javascript; charset=utf-8", Some("bin") => "application/octet-stream", - Some("css") => "text/css", Some("csv") => "text/csv", - Some("html") => "text/html", Some("ico") => "image/vnd.microsoft.icon", - Some("js") => "text/javascript", - Some("json") => "application/json", Some("jsonld") => "application/ld+json", - Some("mjs") => "text/javascript", Some("rtf") => "application/rtf", - Some("svg") => "image/svg+xml", Some("mp4") => "video/mp4", // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com` - Some(_) => "text/html", + Some(_) => "text/html; charset=utf-8", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types // using octet stream according to this: None => "application/octet-stream", } } - -/// A global that stores the workspace root. Used in [`get_workspace_root_from_cargo`]. -static WORKSPACE_ROOT: OnceLock = OnceLock::new(); - -/// Describes the metadata we need from `cargo metadata`. -#[derive(Deserialize)] -struct CargoMetadata { - workspace_root: PathBuf, -} - -/// Get the workspace root using `cargo metadata`. Should not be used in release mode. -pub(crate) fn get_workspace_root_from_cargo() -> PathBuf { - WORKSPACE_ROOT - .get_or_init(|| { - let out = Command::new("cargo") - .args(["metadata", "--format-version", "1", "--no-deps"]) - .output() - .expect("`cargo metadata` failed to run"); - - let out = - String::from_utf8(out.stdout).expect("failed to parse output of `cargo metadata`"); - let metadata = serde_json::from_str::(&out) - .expect("failed to deserialize data from `cargo metadata`"); - - metadata.workspace_root - }) - .to_owned() -} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 62d17718f..cefe9c553 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -55,10 +55,26 @@ impl WebviewEdits { &self, request: wry::http::Request>, ) -> Result, serde_json::Error> { - let response = match serde_json::from_slice(request.body()) { - Ok(event) => self.handle_html_event(event), + let data_from_header = request + .headers() + .get("dioxus-data") + .map(|f| f.as_bytes()) + .expect("dioxus-data header is not a string"); + + let response = match serde_json::from_slice(data_from_header) { + Ok(event) => { + // we need to wait for the mutex lock to let us munge the main thread.. + let _lock = crate::android_sync_lock::android_runtime_lock(); + self.handle_html_event(event) + } Err(err) => { - tracing::error!("Error parsing user_event: {:?}", err); + tracing::error!("cannot decippher format of user event"); + tracing::error!( + "Error parsing user_event: {:?}.Contents: {:?}, raw: {:#?}", + err, + String::from_utf8(request.body().to_vec()), + request + ); SynchronousEventResponse::new(false) } }; @@ -206,19 +222,16 @@ impl WebviewInstance { edits ]; move |request, responder: RequestAsyncResponder| { - // Try to serve the index file first - if let Some(index_bytes) = protocol::index_request( - &request, + protocol::desktop_handler( + request, + asset_handlers.clone(), + responder, + &edits, custom_head.clone(), custom_index.clone(), &root_name, headless, - ) { - return responder.respond(index_bytes); - } - - // Otherwise, try to serve an asset, either from the user or the filesystem - protocol::desktop_handler(request, asset_handlers.clone(), responder, &edits); + ) } }; @@ -417,8 +430,12 @@ impl WebviewInstance { } } - self.dom - .render_immediate(&mut *self.edits.wry_queue.mutation_state_mut()); + // lock the hack-ed in lock sync wry has some thread-safety issues with event handlers + let _lock = crate::android_sync_lock::android_runtime_lock(); + + self.edits + .wry_queue + .with_mutation_state_mut(|f| self.dom.render_immediate(f)); self.edits.wry_queue.send_edits(); } } diff --git a/packages/devtools-types/src/lib.rs b/packages/devtools-types/src/lib.rs index 73989405d..0450e0236 100644 --- a/packages/devtools-types/src/lib.rs +++ b/packages/devtools-types/src/lib.rs @@ -33,9 +33,15 @@ pub enum ClientMsg { }, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct HotReloadMsg { pub templates: Vec, pub assets: Vec, pub unknown_files: Vec, } + +impl HotReloadMsg { + pub fn is_empty(&self) -> bool { + self.templates.is_empty() && self.assets.is_empty() && self.unknown_files.is_empty() + } +} diff --git a/packages/devtools/src/lib.rs b/packages/devtools/src/lib.rs index cdee491f6..d0a0a5f40 100644 --- a/packages/devtools/src/lib.rs +++ b/packages/devtools/src/lib.rs @@ -1,6 +1,6 @@ use dioxus_core::{ScopeId, VirtualDom}; pub use dioxus_devtools_types::*; -use dioxus_signals::Writable; +use dioxus_signals::{GlobalKey, Writable}; use warnings::Warning; /// Applies template and literal changes to the VirtualDom @@ -11,9 +11,14 @@ pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) { let ctx = dioxus_signals::get_global_context(); for template in &msg.templates { - let id = &template.location; let value = template.template.clone(); - if let Some(mut signal) = ctx.get_signal_with_key(id) { + let key = GlobalKey::File { + file: template.key.file.as_str(), + line: template.key.line as _, + column: template.key.column as _, + index: template.key.index as _, + }; + if let Some(mut signal) = ctx.get_signal_with_key(key.clone()) { dioxus_signals::warnings::signal_read_and_write_in_reactive_scope::allow(|| { dioxus_signals::warnings::signal_write_in_component_body::allow(|| { signal.set(Some(value)); diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 8c8638a8e..0025b6898 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -44,7 +44,7 @@ hooks = ["dep:dioxus-hooks"] devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"] mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"] file_engine = ["dioxus-web?/file_engine"] -asset = ["dep:manganis", "dioxus-core/manganis"] +asset = ["dep:manganis"] document = ["dioxus-web?/document", "dioxus-document"] launch = ["dep:dioxus-config-macro"] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index ae7e3984b..dee19e183 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -188,21 +188,35 @@ impl LaunchBuilder { self } + fn launch_inner(self, app: fn() -> Element) { + #[cfg(all(feature = "fullstack", any(feature = "desktop", feature = "mobile")))] + { + use dioxus_fullstack::prelude::server_fn::client::{get_server_url, set_server_url}; + if get_server_url().is_empty() { + let serverurl = format!( + "http://127.0.0.1:{}", + std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()) + ) + .leak(); + set_server_url(serverurl); + } + } + + let cfg = self.configs; + (self.launch_fn)(app, self.contexts, cfg); + } + // Static generation is the only platform that may exit. We can't use the `!` type here #[cfg(any(feature = "static-generation", feature = "web"))] /// Launch your application. pub fn launch(self, app: fn() -> Element) { - let cfg = self.configs; - - (self.launch_fn)(app, self.contexts, cfg) + self.launch_inner(app); } #[cfg(not(any(feature = "static-generation", feature = "web")))] /// Launch your application. pub fn launch(self, app: fn() -> Element) -> ! { - let cfg = self.configs; - - (self.launch_fn)(app, self.contexts, cfg); + self.launch_inner(app); unreachable!("Launching an application will never exit") } } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 43a44dc95..175a9467c 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -132,7 +132,7 @@ pub mod prelude { #[cfg(feature = "asset")] #[cfg_attr(docsrs, doc(cfg(feature = "asset")))] - pub use manganis::{self, classes, mg as asset, ImageAsset, ImageType}; + pub use manganis::{self, asset, Asset, ImageAsset, ImageType}; } #[cfg(feature = "web")] diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 44a88254c..a31f1fc8b 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -84,7 +84,7 @@ file_engine = ["dioxus-web?/file_engine"] document = ["dioxus-web?/document"] web = ["dep:dioxus-web", "dep:web-sys"] desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"] -mobile = ["dep:dioxus-mobile"] +mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"] axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"] diff --git a/packages/fullstack/src/server/mod.rs b/packages/fullstack/src/server/mod.rs index 8dac921b2..8db69c21d 100644 --- a/packages/fullstack/src/server/mod.rs +++ b/packages/fullstack/src/server/mod.rs @@ -214,6 +214,10 @@ where let public_path = crate::public_path(); + if !public_path.exists() { + return self; + } + // Serve all files in public folder except index.html let dir = std::fs::read_dir(&public_path).unwrap_or_else(|e| { panic!( diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 916e19029..931b219f9 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -[6449103750905854967, 4461869229701639737, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 16535067064964671079, 5052021921702764563, 17534315583914394253, 5638004933879392817] \ No newline at end of file +[6449103750905854967, 4461869229701639737, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 10411167459769688501, 5052021921702764563, 17534315583914394253, 5638004933879392817] \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 1f0f889d9..eafa6dbff 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){const detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var toSerializableResizeObserverSize=function(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]};function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var handleVirtualdomEventSync=function(endpoint,contents){const xhr=new XMLHttpRequest;return xhr.open("POST",endpoint,!1),xhr.setRequestHeader("Content-Type","application/json"),xhr.send(contents),JSON.parse(xhr.responseText)},getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;eventsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath,eventsPath){super();this.editsPath=editsPath,this.eventsPath=eventsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getScrollHeight(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer;const file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);const dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){const element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}const dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){const detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var toSerializableResizeObserverSize=function(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]};function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var handleVirtualdomEventSync=function(endpoint,contents){const xhr=new XMLHttpRequest;return xhr.open("POST",endpoint,!1),xhr.setRequestHeader("Content-Type","application/json"),xhr.setRequestHeader("dioxus-data",contents),xhr.send(contents),JSON.parse(xhr.responseText)},getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;eventsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath,eventsPath){super();this.editsPath=editsPath,this.eventsPath=eventsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getScrollHeight(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer;const file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);const dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){const element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}const dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){const dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i Result { + // get the location where the asset is absolute, relative to + // + // IE + // /users/dioxus/dev/app/ + // is the root of + // /users/dioxus/dev/app/assets/blah.css + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .unwrap(); + + // 1. the input file should be a pathbuf + let input = PathBuf::from(raw); + + // 2. absolute path to the asset + let absolute = manifest_dir + .join(raw.trim_start_matches('/')) + .canonicalize() + .unwrap(); + + // 3. the bundled path is the unique name of the asset + let bundled = Self::make_unique_name(absolute.clone()); + + Ok(Self { + input, + absolute, + bundled, + }) + } + + fn make_unique_name(file_path: PathBuf) -> String { + // Create a hasher + let mut hash = std::collections::hash_map::DefaultHasher::new(); + + // Open the file to get its options + let file = std::fs::File::open(&file_path).unwrap(); + let metadata = file.metadata().unwrap(); + let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); + + // Hash a bunch of metadata + // name, options, modified time, and maybe the version of our crate + // Hash the last time the file was updated and the file source. If either of these change, we need to regenerate the unique name + modified.hash(&mut hash); + file_path.hash(&mut hash); + + let uuid = hash.finish(); + let file_name = file_path.file_stem().unwrap().to_string_lossy(); + let extension = file_path + .extension() + .map(|f| f.to_string_lossy()) + .unwrap_or_default(); + + format!("{file_name}-{uuid:x}.{extension}") + } +} diff --git a/packages/manganis/manganis-core/src/lib.rs b/packages/manganis/manganis-core/src/lib.rs new file mode 100644 index 000000000..4f5c4d752 --- /dev/null +++ b/packages/manganis/manganis-core/src/lib.rs @@ -0,0 +1,5 @@ +mod asset; +mod linker; + +pub use asset::*; +pub use linker::*; diff --git a/packages/manganis/manganis-core/src/linker.rs b/packages/manganis/manganis-core/src/linker.rs new file mode 100644 index 000000000..e86444cf4 --- /dev/null +++ b/packages/manganis/manganis-core/src/linker.rs @@ -0,0 +1,69 @@ +/// Information about the manganis link section for a given platform +#[derive(Debug, Clone, Copy)] +pub struct LinkSection { + /// The link section we pass to the static + pub link_section: &'static str, + /// The name of the section we find in the binary + pub name: &'static str, +} + +impl LinkSection { + /// The list of link sections for all supported platforms + pub const ALL: &'static [&'static LinkSection] = + &[Self::WASM, Self::MACOS, Self::WINDOWS, Self::ILLUMOS]; + + /// Returns the link section used in linux, android, fuchsia, psp, freebsd, and wasm32 + pub const WASM: &'static LinkSection = &LinkSection { + link_section: "manganis", + name: "manganis", + }; + + /// Returns the link section used in macOS, iOS, tvOS + pub const MACOS: &'static LinkSection = &LinkSection { + link_section: "__DATA,manganis,regular,no_dead_strip", + name: "manganis", + }; + + /// Returns the link section used in windows + pub const WINDOWS: &'static LinkSection = &LinkSection { + link_section: "mg", + name: "mg", + }; + + /// Returns the link section used in illumos + pub const ILLUMOS: &'static LinkSection = &LinkSection { + link_section: "set_manganis", + name: "set_manganis", + }; + + /// The link section used on the current platform + pub const CURRENT: &'static LinkSection = { + #[cfg(any( + target_os = "none", + target_os = "linux", + target_os = "android", + target_os = "fuchsia", + target_os = "psp", + target_os = "freebsd", + target_arch = "wasm32" + ))] + { + Self::WASM + } + + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] + { + Self::MACOS + } + + #[cfg(target_os = "windows")] + { + Self::WINDOWS + } + + #[cfg(target_os = "illumos")] + { + Self::ILLUMOS + } + }; +} diff --git a/packages/manganis/manganis-macro/Cargo.toml b/packages/manganis/manganis-macro/Cargo.toml new file mode 100644 index 000000000..7a5f7ffd3 --- /dev/null +++ b/packages/manganis/manganis-macro/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "manganis-macro" +version.workspace = true +edition = "2021" +authors = ["Evan Almloff"] +description = "Ergonomic, automatic, cross crate asset collection and optimization" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/manganis/" +homepage = "https://dioxuslabs.com" +keywords = ["assets"] + +[lib] +proc-macro = true + +# be careful with dependencies you add here - these need to get compiled for the proc macro and therefore +# prevent the main code from compiling! +[dependencies] +proc-macro2 = { version = "1.0" } +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +serde_json = "1.0" +serde = { workspace = true, features = ["derive"] } +manganis-core = { workspace = true } + +[features] +default = [] diff --git a/packages/manganis/manganis-macro/README.md b/packages/manganis/manganis-macro/README.md new file mode 100644 index 000000000..fde983fef --- /dev/null +++ b/packages/manganis/manganis-macro/README.md @@ -0,0 +1,3 @@ +# Manganis Macro + +This crate contains the macro used to interact with the Manganis asset system. diff --git a/packages/manganis/manganis-macro/src/asset.rs b/packages/manganis/manganis-macro/src/asset.rs new file mode 100644 index 000000000..a77325b68 --- /dev/null +++ b/packages/manganis/manganis-macro/src/asset.rs @@ -0,0 +1,96 @@ +use manganis_core::ResourceAsset; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + LitStr, +}; + +pub struct AssetParser { + /// The asset itself + asset: ResourceAsset, + + /// The source of the trailing options + options: TokenStream2, +} + +impl Parse for AssetParser { + // we can take + // + // This gives you the Asset type - it's generic and basically unrefined + // ``` + // asset!("myfile.png") + // ``` + // + // To narrow the type, use a method call to get the refined type + // ``` + // asset!( + // "myfile.png", + // asset::image() + // .format(ImageType::Jpg) + // .size(512, 512) + // ) + // ``` + // + // But we need to decide the hint first before parsing the options + fn parse(input: ParseStream) -> syn::Result { + // And then parse the options + let src = input.parse::()?; + let asset = ResourceAsset::parse_any(&src.value()).unwrap(); + let options = input.parse()?; + + Ok(Self { asset, options }) + } +} + +impl ToTokens for AssetParser { + // Need to generate: + // + // - 1. absolute file path on the user's system: `/users/dioxus/dev/project/assets/blah.css` + // - 2. original input in case that's useful: `../blah.css` + // - 3. path relative to the CARGO_MANIFEST_DIR - and then we'll add a `/`: `/assets/blah.css + // - 4. file from which this macro was called: `/users/dioxus/dev/project/src/lib.rs` + // - 5: The link section containing all this data + // - 6: the input tokens such that the builder gets validated by the const code + // - 7: the bundled name `/blahcss123.css` + // + // Not that we'll use everything, but at least we have this metadata for more post-processing. + // + // For now, `2` and `3` will be the same since we don't support relative paths... a bit of + // a limitation from rust itself. We technically could support them but not without some hoops + // to jump through + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + // 1. the link section itself + let link_section = crate::generate_link_section(&self.asset); + + // 2. original + let input = self.asset.input.display().to_string(); + + // 3. resolved on the user's system + let local = self.asset.absolute.display().to_string(); + + // 4. bundled + let bundled = self.asset.bundled.to_string(); + + // 5. source tokens + let option_source = &self.options; + + tokens.extend(quote! { + Asset::new( + { + #link_section + manganis::Asset { + // "/assets/blah.css" + input: #input, + + // "/users/dioxus/dev/app/assets/blah.css" + local: #local, + + // "/blahcss123.css" + bundled: #bundled, + } + } + ) #option_source + }) + } +} diff --git a/packages/manganis/manganis-macro/src/lib.rs b/packages/manganis/manganis-macro/src/lib.rs new file mode 100644 index 000000000..05d9d9850 --- /dev/null +++ b/packages/manganis/manganis-macro/src/lib.rs @@ -0,0 +1,19 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; +use syn::parse_macro_input; + +pub(crate) mod asset; +pub(crate) mod linker; + +use linker::generate_link_section; + +/// The asset macro collects assets that will be included in the final binary +#[proc_macro] +pub fn asset(input: TokenStream) -> TokenStream { + let asset = parse_macro_input!(input as asset::AssetParser); + + quote! { #asset }.into_token_stream().into() +} diff --git a/packages/manganis/manganis-macro/src/linker.rs b/packages/manganis/manganis-macro/src/linker.rs new file mode 100644 index 000000000..8f3b7cfa5 --- /dev/null +++ b/packages/manganis/manganis-macro/src/linker.rs @@ -0,0 +1,21 @@ +use proc_macro2::TokenStream as TokenStream2; +use serde::Serialize; + +/// this new approach will store the assets descriptions *inside the executable*. +/// The trick is to use the `link_section` attribute. +/// We force rust to store a json representation of the asset description +/// inside a particular region of the binary, with the label "manganis". +/// After linking, the "manganis" sections of the different executables will be merged. +pub fn generate_link_section(asset: &impl Serialize) -> TokenStream2 { + let position = proc_macro2::Span::call_site(); + let asset_description = serde_json::to_string(asset).unwrap(); + let len = asset_description.as_bytes().len(); + let asset_bytes = syn::LitByteStr::new(asset_description.as_bytes(), position); + let section_name = syn::LitStr::new(manganis_core::LinkSection::CURRENT.link_section, position); + + quote::quote! { + #[link_section = #section_name] + #[used] + static ASSET: [u8; #len] = * #asset_bytes; + } +} diff --git a/packages/manganis/manganis/Cargo.toml b/packages/manganis/manganis/Cargo.toml new file mode 100644 index 000000000..82d74cc67 --- /dev/null +++ b/packages/manganis/manganis/Cargo.toml @@ -0,0 +1,41 @@ +[package] +# Manganese is a rusting catalyst. Manganis makes it faster to collect rust assets (and has almost no google search results) +name = "manganis" +version.workspace = true +authors = ["Evan Almloff"] +edition = "2021" +description = "Ergonomic, automatic, cross crate asset collection and optimization" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/manganis/" +homepage = "https://dioxuslabs.com" +keywords = ["assets"] + +[lib] + +[dependencies] +manganis-macro = { workspace = true, optional = true } +dioxus-core-types = { workspace = true } + +# dunce = "1.0.2" +# [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +# core-foundation = "0.10.0" +# [target.'cfg(target_os = "macos")'.dependencies] +# core-foundation = "0.9.3" +# [target.'cfg(target_os ="macos")'.dependencies] +# infer = { workspace = true } +# dirs = "5.0.1" +# infer = { workspace = true } +# manganis-common = { workspace = true } + +once_cell = "1.19.0" +dunce = "1.0.2" +serde = { version = "1.0.183", features = ["derive"] } +anyhow = "1" +base64 = { workspace = true } + + +[features] +default = ["macro"] +html = [] +# url-encoding = ["manganis-macro/url-encoding"] +macro = ["dep:manganis-macro"] diff --git a/packages/manganis/manganis/src/builder.rs b/packages/manganis/manganis/src/builder.rs new file mode 100644 index 000000000..1441fd734 --- /dev/null +++ b/packages/manganis/manganis/src/builder.rs @@ -0,0 +1,71 @@ +use dioxus_core_types::DioxusFormattable; +use std::path::PathBuf; + +/// Asset +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] +pub struct Asset { + /// The input URI given to the macro + pub input: &'static str, + + /// The absolute path to the asset on the filesystem + pub local: &'static str, + + /// The asset location after its been bundled + /// + /// `blah-123.css`` + pub bundled: &'static str, +} + +impl Asset { + /// Create a new asset + pub const fn new(self) -> Self { + self + } + + /// Get the path to the asset + pub fn path(&self) -> PathBuf { + PathBuf::from(self.input.to_string()) + } + + /// Get the path to the asset + pub fn relative_path(&self) -> PathBuf { + PathBuf::from(self.input.trim_start_matches('/').to_string()) + } + + /// Return a canonicalized path to the asset + /// + /// Attempts to resolve it against an `assets` folder in the current directory. + /// If that doesn't exist, it will resolve against the cargo manifest dir + pub fn resolve(&self) -> PathBuf { + // If the asset is relative, we resolve the asset at the current directory + if !dioxus_core_types::is_bundled_app() { + return PathBuf::from(self.local); + } + + // Otherwise presumably we're bundled and we can use the bundled path + PathBuf::from("/assets/").join(PathBuf::from(self.bundled.trim_start_matches('/'))) + } +} + +impl From for String { + fn from(value: Asset) -> Self { + value.to_string() + } +} +impl From for Option { + fn from(value: Asset) -> Self { + Some(value.to_string()) + } +} + +impl std::fmt::Display for Asset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.resolve().display()) + } +} + +impl DioxusFormattable for Asset { + fn format(&self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Owned(self.to_string()) + } +} diff --git a/packages/manganis/manganis/src/folder.rs b/packages/manganis/manganis/src/folder.rs new file mode 100644 index 000000000..971b6927a --- /dev/null +++ b/packages/manganis/manganis/src/folder.rs @@ -0,0 +1,31 @@ +use crate::Asset; + +/// This is basically a compile-time version of ResourceAsset +/// A struct that contains the relative and absolute paths of an asset +#[derive(Debug, PartialEq, PartialOrd, Clone, Hash)] +pub struct FolderAsset { + src: Asset, +} + +impl Asset { + /// Create a new folder using `Asset` as the base type + pub const fn folder(self) -> FolderAsset { + FolderAsset::new(self) + } +} + +impl FolderAsset { + /// Create a new folder asset from an `Asset` + pub const fn new(src: Asset) -> Self { + Self { src } + } +} + +/// The builder for [`FolderAsset`] +pub struct FolderAssetBuilder; + +/// Create an folder asset from the local path +#[allow(unused)] +pub const fn folder(src: &'static str) -> FolderAssetBuilder { + FolderAssetBuilder {} +} diff --git a/packages/manganis/manganis/src/images.rs b/packages/manganis/manganis/src/images.rs new file mode 100644 index 000000000..b5a83060e --- /dev/null +++ b/packages/manganis/manganis/src/images.rs @@ -0,0 +1,141 @@ +use dioxus_core_types::DioxusFormattable; + +use crate::Asset; + +/// An image asset that is built by the [`asset!`] macro +#[derive(Debug, PartialEq, PartialOrd, Clone, Hash, Copy)] +pub struct ImageAsset { + /// The path to the image + asset: Asset, + /// A low quality preview of the image that is URL encoded + preview: Option<&'static str>, + /// A caption for the image + caption: Option<&'static str>, + /// The format of the image + format: Option, +} + +impl Asset { + /// Convert this asset into an image asset + pub const fn image(self) -> ImageAsset { + ImageAsset::new(self) + } +} + +impl ImageAsset { + /// Creates a new image asset + pub const fn new(path: Asset) -> Self { + Self { + asset: path, + preview: None, + caption: None, + format: None, + } + } + + /// Returns the path to the image + pub const fn path(&self) -> &'static str { + self.asset.bundled + } + + /// Returns the preview of the image + pub const fn preview(&self) -> Option<&'static str> { + self.preview + } + + /// Sets the preview of the image + pub const fn with_preview(self, preview: Option<&'static str>) -> Self { + Self { preview, ..self } + } + + /// Returns the caption of the image + pub const fn caption(&self) -> Option<&'static str> { + self.caption + } + + /// Sets the caption of the image + pub const fn with_caption(self, caption: Option<&'static str>) -> Self { + Self { caption, ..self } + } + + /// Sets the format of the image + pub const fn format(self, format: Option) -> Self { + Self { format, ..self } + } +} + +impl std::ops::Deref for ImageAsset { + type Target = Asset; + + fn deref(&self) -> &Self::Target { + &self.asset + } +} + +impl std::fmt::Display for ImageAsset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.asset.fmt(f) + } +} + +impl DioxusFormattable for ImageAsset { + fn format(&self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Owned(self.to_string()) + } +} + +/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types) +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] +pub enum ImageType { + /// A png image. Png images cannot contain transparency and tend to compress worse than other formats + Png, + /// A jpg image. Jpg images can contain transparency and tend to compress better than png images + Jpg, + /// A webp image. Webp images can contain transparency and tend to compress better than jpg images + Webp, + /// An avif image. Avif images can compress slightly better than webp images but are not supported by all browsers + Avif, +} + +/// A builder for an image asset. This must be used in the [`asset!`] macro. +/// +/// > **Note**: This will do nothing outside of the `asset!` macro +pub struct ImageAssetBuilder; + +impl ImageAssetBuilder { + /// Sets the format of the image + #[allow(unused)] + pub const fn format(self, format: ImageType) -> Self { + Self + } + + /// Sets the size of the image + #[allow(unused)] + pub const fn size(self, x: u32, y: u32) -> Self { + Self + } + + /// Make the image use a low quality preview + #[allow(unused)] + pub const fn low_quality_preview(self) -> Self { + Self + } + + /// Make the image preloaded + #[allow(unused)] + pub const fn preload(self) -> Self { + Self + } + + /// Make the image URL encoded + #[allow(unused)] + pub const fn url_encoded(self) -> Self { + Self + } +} + +/// Create an image asset from the local path or url to the image +#[allow(unused)] +pub const fn image(path: &'static str) -> ImageAssetBuilder { + ImageAssetBuilder +} diff --git a/packages/manganis/manganis/src/lib.rs b/packages/manganis/manganis/src/lib.rs new file mode 100644 index 000000000..9302470f7 --- /dev/null +++ b/packages/manganis/manganis/src/lib.rs @@ -0,0 +1,15 @@ +//! Manganis is a Rust library for bundling assets into a final binary. + +#![deny(missing_docs)] + +#[cfg(feature = "macro")] +pub use manganis_macro::*; + +mod folder; +pub use folder::*; + +mod images; +pub use images::*; + +mod builder; +pub use builder::*; diff --git a/packages/mobile/.cargo/config.toml b/packages/mobile/.cargo/config.toml index bef33769e..2a9579151 100644 --- a/packages/mobile/.cargo/config.toml +++ b/packages/mobile/.cargo/config.toml @@ -1,2 +1,5 @@ [build] -target = "aarch64-apple-ios" +target = [ + "aarch64-apple-ios", + "aarch64-linux-android" +] diff --git a/packages/mobile/.vscode/settings.json b/packages/mobile/.vscode/settings.json index 433a1b335..917c00cff 100644 --- a/packages/mobile/.vscode/settings.json +++ b/packages/mobile/.vscode/settings.json @@ -1,5 +1,4 @@ { "rust-analyzer.inlayHints.enable": true, "rust-analyzer.checkOnSave.allTargets": true, - "rust-analyzer.cargo.target": "aarch64-apple-ios" } diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 1a058c7ce..c0976058f 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -11,6 +11,14 @@ license = "MIT OR Apache-2.0" [dependencies] dioxus-desktop = { workspace = true } +dioxus-lib = { workspace = true } +jni = "0.21.1" +libc = "0.2.159" +once_cell.workspace = true + +[features] +default = ["jnibindings"] +jnibindings = [] [lib] doctest = false diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 5cc2c6ae9..0be7e6b26 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -3,3 +3,59 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub use dioxus_desktop::*; +use dioxus_lib::prelude::*; +use std::sync::Mutex; + +/// Launch via the binding API +pub fn launch(incoming: fn() -> Element) { + *APP_FN_PTR.lock().unwrap() = Some(incoming); +} + +static APP_FN_PTR: Mutex Element>> = Mutex::new(None); + +pub fn root() { + let app = APP_FN_PTR.lock().unwrap().unwrap(); + dioxus_desktop::launch::launch(app, vec![], Default::default()); +} + +#[cfg(target_os = "android")] +#[no_mangle] +#[inline(never)] +pub extern "C" fn start_app() { + tao::android_binding!( + com_example, + wrytest, + WryActivity, + wry::android_setup, + root, + tao + ); + wry::android_binding!(com_example, wrytest, wry); +} + +/// Call our `main` function to initialize the rust runtime and set the launch binding trampoline +#[cfg(target_os = "android")] +#[no_mangle] +#[inline(never)] +pub extern "C" fn JNI_OnLoad( + _vm: *mut libc::c_void, + _reserved: *mut libc::c_void, +) -> jni::sys::jint { + // we're going to find the `main` symbol using dlsym directly and call it + unsafe { + let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _); + + if main_fn_ptr.is_null() { + main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _); + } + + if main_fn_ptr.is_null() { + panic!("Failed to find main symbol"); + } + + let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr); + main_fn(); + }; + + jni::sys::JNI_VERSION_1_6 +} diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs index 6dea889cc..cbd3d0ef8 100644 --- a/packages/playwright-tests/fullstack/src/main.rs +++ b/packages/playwright-tests/fullstack/src/main.rs @@ -16,8 +16,8 @@ fn app() -> Element { let mut text = use_signal(|| "...".to_string()); rsx! { - h1 { "hello axum! {count}" } document::Title { "hello axum! {count}" } + h1 { "hello axum! {count}" } button { class: "increment-button", onclick: move |_| count += 1, "Increment" } button { class: "server-button", diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 9efa89c8d..522999b36 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -87,25 +87,16 @@ module.exports = defineConfig({ { cwd: path.join(process.cwd(), "web"), command: - 'cargo run --package dioxus-cli --release -- serve --addr "127.0.0.1" --port 9999', + 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 9999', port: 9999, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, - { - cwd: path.join(process.cwd(), "static-generation"), - command: - 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform static-generation --addr "127.0.0.1" --port 2222', - port: 2222, - timeout: 50 * 60 * 1000, - reuseExistingServer: !process.env.CI, - stdout: "pipe", - }, { cwd: path.join(process.cwd(), "fullstack"), command: - 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform fullstack --addr "127.0.0.1" --port 3333', + 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 3333', port: 3333, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, @@ -114,7 +105,7 @@ module.exports = defineConfig({ { cwd: path.join(process.cwd(), "suspense-carousel"), command: - 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform fullstack --addr "127.0.0.1" --port 4040', + 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 4040', port: 4040, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, @@ -123,7 +114,7 @@ module.exports = defineConfig({ { cwd: path.join(process.cwd(), "nested-suspense"), command: - 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform fullstack --addr "127.0.0.1" --port 5050', + 'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 5050', port: 5050, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, diff --git a/packages/playwright-tests/static-generation.spec.js b/packages/playwright-tests/static-generation.spec.js deleted file mode 100644 index 9a4f03911..000000000 --- a/packages/playwright-tests/static-generation.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-check -const { test, expect } = require("@playwright/test"); - -test("button click", async ({ page }) => { - await page.goto("http://localhost:2222"); - await page.waitForTimeout(1000); - - // Expect the page to contain the counter text. - const main = page.locator("#main"); - await expect(main).toContainText("hello axum! 12345"); - - // Expect the page to contain the server data - await expect(main).toContainText('Server said: Ok("Hello from the server!")'); - - // Click the increment button. - let button = page.locator("button.increment-button"); - await button.click(); - - // Expect the page to contain the updated counter text. - await expect(main).toContainText("hello axum! 12346"); -}); diff --git a/packages/playwright-tests/static-generation/.gitignore b/packages/playwright-tests/static-generation/.gitignore deleted file mode 100644 index 4a7bcb809..000000000 --- a/packages/playwright-tests/static-generation/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.dioxus -dist -target \ No newline at end of file diff --git a/packages/playwright-tests/static-generation/Cargo.toml b/packages/playwright-tests/static-generation/Cargo.toml deleted file mode 100644 index e66b07355..000000000 --- a/packages/playwright-tests/static-generation/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "dioxus-playwright-static-generation-test" -version = "0.1.0" -edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -dioxus = { workspace = true, features = ["static-generation"] } -serde = "1.0.159" -tokio = { workspace = true, features = ["full"], optional = true } - -[features] -default = [] -server = ["dioxus/axum", "tokio"] -web = ["dioxus/web"] diff --git a/packages/playwright-tests/static-generation/src/main.rs b/packages/playwright-tests/static-generation/src/main.rs deleted file mode 100644 index 7540b226b..000000000 --- a/packages/playwright-tests/static-generation/src/main.rs +++ /dev/null @@ -1,33 +0,0 @@ -// This test is used by playwright configured in the root of the repo -// Tests: -// - Static Generation -// - Simple Suspense -// - Hydration - -#![allow(non_snake_case)] -use dioxus::prelude::*; - -fn main() { - #[cfg(feature = "web")] - dioxus::LaunchBuilder::web().launch(app); - #[cfg(feature = "server")] - dioxus::LaunchBuilder::static_generation().launch(app); -} - -#[allow(unused)] -fn app() -> Element { - let mut count = use_signal(|| 12345); - let server_data = use_server_future(get_server_data)?; - - rsx! { - h1 { "hello axum! {count}" } - button { class: "increment-button", onclick: move |_| count += 1, "Increment" } - "Server said: {server_data().unwrap():?}" - } -} - -#[server(GetServerData)] -async fn get_server_data() -> Result { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - Ok("Hello from the server!".to_string()) -} diff --git a/packages/rsx-hotreload/src/collect.rs b/packages/rsx-hotreload/src/collect.rs index ce1c1a73c..e42bfd04d 100644 --- a/packages/rsx-hotreload/src/collect.rs +++ b/packages/rsx-hotreload/src/collect.rs @@ -11,7 +11,6 @@ use syn::visit_mut::VisitMut; use syn::{File, Macro}; -#[derive(Debug)] pub struct ChangedRsx { /// The old macro - the original RSX from the original file pub old: Macro, @@ -20,6 +19,11 @@ pub struct ChangedRsx { pub new: Macro, } +#[derive(Debug)] +pub enum ReloadableRustCode { + Rsx { old: Macro, new: Macro }, +} + /// Find any rsx calls in the given file and return a list of all the rsx calls that have changed. /// /// Takes in the two files, clones them, removes the rsx! contents and prunes any doc comments. diff --git a/packages/rsx-hotreload/src/diff.rs b/packages/rsx-hotreload/src/diff.rs index b408e2be5..f86e2b102 100644 --- a/packages/rsx-hotreload/src/diff.rs +++ b/packages/rsx-hotreload/src/diff.rs @@ -81,9 +81,6 @@ use super::last_build_state::LastBuildState; #[non_exhaustive] #[derive(Debug, PartialEq, Clone)] pub struct HotReloadResult { - /// The state of the last full rebuild. - full_rebuild_state: LastBuildState, - /// The child templates we have already used. As we walk through the template tree, we will run into child templates. /// Each of those child templates also need to be hot reloaded. We keep track of which ones we've already hotreloaded /// to avoid diffing the same template twice against different new templates. @@ -105,6 +102,9 @@ pub struct HotReloadResult { /// You should diff the result of this against the old template to see if you actually need to send down the result pub templates: HashMap, + /// The state of the last full rebuild. + full_rebuild_state: LastBuildState, + /// The dynamic nodes for the current node dynamic_nodes: Vec, diff --git a/packages/rsx/src/template_body.rs b/packages/rsx/src/template_body.rs index 073a4f077..bd14dad37 100644 --- a/packages/rsx/src/template_body.rs +++ b/packages/rsx/src/template_body.rs @@ -156,15 +156,20 @@ impl ToTokens for TemplateBody { } __ORIGINAL_TEMPLATE.get().unwrap() } + + static __NORMALIZED_FILE: &'static str = { + const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/"); + dioxus_core::const_format::str_replace!(PATH, '\\', "/") + }; + // The key is important here - we're creating a new GlobalSignal each call to this // But the key is what's keeping it stable - let __template = GlobalSignal::with_key( + let __template = GlobalSignal::with_location( || None::, - { - const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/"); - const NORMAL: &str = dioxus_core::const_format::str_replace!(PATH, '\\', "/"); - dioxus_core::const_format::concatcp!(NORMAL, ':', line!(), ':', column!(), ':', #index) - } + __NORMALIZED_FILE, + line!(), + column!(), + #index ); // If the template has not been hot reloaded, we always use the original template diff --git a/packages/signals/src/global/mod.rs b/packages/signals/src/global/mod.rs index e102fc3b9..e090d96c4 100644 --- a/packages/signals/src/global/mod.rs +++ b/packages/signals/src/global/mod.rs @@ -137,10 +137,38 @@ where /// This is useful for ensuring that the signal is unique across the application and accessible from /// outside the application too. #[track_caller] - pub const fn with_key(constructor: fn() -> R, key: &'static str) -> Self { + pub const fn with_name(constructor: fn() -> R, key: &'static str) -> Self { Self { constructor, - key: GlobalKey::new_from_str(key), + key: GlobalKey::File { + file: key, + line: 0, + column: 0, + index: 0, + }, + phantom: std::marker::PhantomData, + } + } + + /// Create this global signal with a specific key. + /// This is useful for ensuring that the signal is unique across the application and accessible from + /// outside the application too. + #[track_caller] + pub const fn with_location( + constructor: fn() -> R, + file: &'static str, + line: u32, + column: u32, + index: usize, + ) -> Self { + Self { + constructor, + key: GlobalKey::File { + file, + line: line as _, + column: column as _, + index: index as _, + }, phantom: std::marker::PhantomData, } } @@ -188,38 +216,36 @@ pub struct GlobalLazyContext { /// A key used to identify a signal in the global signal context #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct GlobalKey<'a> { - // We create an extra wrapper around location so we can construct it manually for hot reloading - file: &'a str, - line: u32, - column: u32, +pub enum GlobalKey<'a> { + /// A key derived from a `std::panic::Location` type + File { + /// The file name + file: &'a str, + + /// The line number + line: u32, + + /// The column number + column: u32, + + /// The index of the signal in the file - used to disambiguate macro calls + index: u32, + }, + + /// A raw key derived just from a string + Raw(&'a str), } impl<'a> GlobalKey<'a> { /// Create a new key from a location pub const fn new(key: &'a Location<'a>) -> Self { - GlobalKey { + GlobalKey::File { file: key.file(), line: key.line(), column: key.column(), + index: 0, } } - - /// Create a new key from a static string - #[allow(unused)] - pub const fn new_from_str(key: &'a str) -> Self { - GlobalKey { - file: key, - line: 0, - column: 0, - } - } -} - -impl From<&'static str> for GlobalKey<'static> { - fn from(key: &'static str) -> Self { - Self::new_from_str(key) - } } impl From<&'static Location<'static>> for GlobalKey<'static> { @@ -231,9 +257,7 @@ impl From<&'static Location<'static>> for GlobalKey<'static> { impl GlobalLazyContext { /// Get a signal with the given string key /// The key will be converted to a UUID with the appropriate internal namespace - pub fn get_signal_with_key(&self, key: &str) -> Option> { - let key = GlobalKey::new_from_str(key); - + pub fn get_signal_with_key(&self, key: GlobalKey) -> Option> { self.map.borrow().get(&key).map(|f| { *f.downcast_ref::>().unwrap_or_else(|| { panic!( @@ -265,7 +289,7 @@ mod tests { // we're using consts since it's harder than statics due to merging - these won't be merged const MYSIGNAL: GlobalSignal = GlobalSignal::new(|| 42); const MYSIGNAL2: GlobalSignal = GlobalSignal::new(|| 42); - const MYSIGNAL3: GlobalSignal = GlobalSignal::with_key(|| 42, "custom-keyed"); + const MYSIGNAL3: GlobalSignal = GlobalSignal::with_name(|| 42, "custom-keyed"); let a = MYSIGNAL.key(); let b = MYSIGNAL.key();