浏览代码

Merge remote-tracking branch 'origin/main' into macos_pos

Jonathan Kelley 1 周之前
父节点
当前提交
28c2d31d04
共有 100 个文件被更改,包括 6975 次插入4864 次删除
  1. 14 0
      .github/actions/free-disk-space/action.yml
  2. 56 0
      .github/install.ps1
  3. 104 0
      .github/install.sh
  4. 66 25
      .github/workflows/main.yml
  5. 1 1
      .github/workflows/merge.yml
  6. 17 7
      .github/workflows/publish.yml
  7. 2 0
      .gitignore
  8. 4 1
      .vscode/settings.json
  9. 894 714
      Cargo.lock
  10. 70 64
      Cargo.toml
  11. 39 8
      README.md
  12. 26 0
      SECURITY.md
  13. 5 0
      _typos.toml
  14. 3 2
      example-projects/ecommerce-site/README.md
  15. 0 3
      example-projects/ecommerce-site/input.css
  16. 865 1195
      example-projects/ecommerce-site/public/tailwind.css
  17. 2 2
      example-projects/ecommerce-site/src/components/nav.rs
  18. 1 1
      example-projects/ecommerce-site/src/components/product_page.rs
  19. 0 13
      example-projects/ecommerce-site/tailwind.config.js
  20. 2 0
      example-projects/ecommerce-site/tailwind.css
  21. 6 6
      examples/README.md
  22. 0 379
      examples/assets/todomvc-native.css
  23. 5 0
      examples/assets/todomvc.css
  24. 6 0
      examples/form.rs
  25. 1 1
      examples/fullstack-auth/Cargo.toml
  26. 1 1
      examples/fullstack-auth/src/auth.rs
  27. 11 10
      examples/fullstack-auth/src/main.rs
  28. 9 9
      examples/fullstack-desktop/src/main.rs
  29. 7 7
      examples/fullstack-hello-world/src/main.rs
  30. 17 17
      examples/fullstack-router/src/main.rs
  31. 0 1
      examples/fullstack-streaming/Cargo.toml
  32. 7 7
      examples/fullstack-streaming/src/main.rs
  33. 4 0
      examples/fullstack-websockets/.gitignore
  34. 16 0
      examples/fullstack-websockets/Cargo.toml
  35. 69 0
      examples/fullstack-websockets/src/main.rs
  36. 16 5
      examples/future.rs
  37. 2 0
      examples/tailwind/README.md
  38. 0 3
      examples/tailwind/input.css
  39. 431 799
      examples/tailwind/public/tailwind.css
  40. 5 5
      examples/tailwind/src/main.rs
  41. 0 9
      examples/tailwind/tailwind.config.js
  42. 2 0
      examples/tailwind/tailwind.css
  43. 0 269
      examples/todomvc-native.rs
  44. 23 0
      examples/wgpu-texture/Cargo.toml
  45. 254 0
      examples/wgpu-texture/src/demo_renderer.rs
  46. 109 0
      examples/wgpu-texture/src/main.rs
  47. 111 0
      examples/wgpu-texture/src/shader.wgsl
  48. 62 0
      examples/wgpu-texture/src/styles.css
  49. 2 1
      lychee.toml
  50. 二进制
      notes/primitive-components.avif
  51. 205 0
      notes/releases/0.7.0-alpha.0.md
  52. 1 1
      packages/asset-resolver/Cargo.toml
  53. 13 3
      packages/asset-resolver/src/lib.rs
  54. 2 2
      packages/autofmt/tests/srcless/basic_expr.rsx
  55. 5 2
      packages/cli-config/src/lib.rs
  56. 3 0
      packages/cli-opt/Cargo.toml
  57. 3 0
      packages/cli-opt/build.rs
  58. 10 0
      packages/cli-opt/src/build_info.rs
  59. 32 6
      packages/cli-opt/src/css.rs
  60. 90 48
      packages/cli-opt/src/file.rs
  61. 1 1
      packages/cli-opt/src/folder.rs
  62. 166 0
      packages/cli-opt/src/hash.rs
  63. 23 12
      packages/cli-opt/src/image/mod.rs
  64. 62 15
      packages/cli-opt/src/js.rs
  65. 69 76
      packages/cli-opt/src/lib.rs
  66. 13 16
      packages/cli/Cargo.toml
  67. 0 2
      packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs
  68. 1 1
      packages/cli/assets/web/dev.index.html
  69. 1 0
      packages/cli/assets/web/prod.index.html
  70. 370 0
      packages/cli/src/build/assets.rs
  71. 402 169
      packages/cli/src/build/builder.rs
  72. 9 3
      packages/cli/src/build/context.rs
  73. 4 0
      packages/cli/src/build/mod.rs
  74. 162 45
      packages/cli/src/build/patch.rs
  75. 148 0
      packages/cli/src/build/pre_render.rs
  76. 977 406
      packages/cli/src/build/request.rs
  77. 20 4
      packages/cli/src/build/tools.rs
  78. 69 116
      packages/cli/src/cli/build.rs
  79. 5 9
      packages/cli/src/cli/build_assets.rs
  80. 2 148
      packages/cli/src/cli/bundle.rs
  81. 3 5
      packages/cli/src/cli/check.rs
  82. 63 9
      packages/cli/src/cli/create.rs
  83. 15 4
      packages/cli/src/cli/init.rs
  84. 27 15
      packages/cli/src/cli/link.rs
  85. 70 22
      packages/cli/src/cli/mod.rs
  86. 169 0
      packages/cli/src/cli/platform_override.rs
  87. 46 11
      packages/cli/src/cli/run.rs
  88. 21 14
      packages/cli/src/cli/serve.rs
  89. 24 92
      packages/cli/src/cli/target.rs
  90. 247 0
      packages/cli/src/cli/update.rs
  91. 13 1
      packages/cli/src/cli/verbosity.rs
  92. 23 5
      packages/cli/src/config/app.rs
  93. 1 1
      packages/cli/src/config/bundle.rs
  94. 0 5
      packages/cli/src/config/desktop.rs
  95. 6 7
      packages/cli/src/config/dioxus_config.rs
  96. 0 2
      packages/cli/src/config/mod.rs
  97. 10 0
      packages/cli/src/config/web.rs
  98. 0 10
      packages/cli/src/devcfg.rs
  99. 10 4
      packages/cli/src/error.rs
  100. 12 7
      packages/cli/src/logging.rs

+ 14 - 0
.github/actions/free-disk-space/action.yml

@@ -0,0 +1,14 @@
+name: Free Disk Space
+description: Free up disk space on the runner
+runs:
+  using: composite
+  steps:
+  - name: Free Disk Space (Ubuntu)
+    if: runner.os == 'Linux'
+    shell: bash
+    run: |
+      echo "Freeing up disk space..."
+      sudo rm -rf /opt/ghc
+      sudo rm -rf /usr/share/dotnet
+      sudo rm -rf /usr/local/lib/android
+      sudo rm -rf /usr/share/swift

+ 56 - 0
.github/install.ps1

@@ -0,0 +1,56 @@
+#!/usr/bin/env pwsh
+
+$ErrorActionPreference = 'Stop'
+
+if ($v) {
+  $Version = "v${v}"
+}
+if ($Args.Length -eq 1) {
+  $Version = $Args.Get(0)
+}
+
+$DxInstall = $env:DX_INSTALL
+$BinDir = if ($DxInstall) {
+  "${DxInstall}\bin"
+} else {
+  "${Home}\.dx\bin"
+}
+
+$DxZip = "$BinDir\dx.zip"
+$DxExe = "$BinDir\dx.exe"
+$Target = 'x86_64-pc-windows-msvc'
+
+$DownloadUrl = if (!$Version) {
+    "https://github.com/dioxuslabs/dioxus/releases/latest/download/dx-${target}.zip"
+} else {
+    "https://github.com/dioxuslabs/dioxus/releases/download/${Version}/dx-${target}.zip"
+}
+
+
+if (!(Test-Path $BinDir)) {
+  New-Item $BinDir -ItemType Directory | Out-Null
+}
+
+curl.exe --ssl-revoke-best-effort -Lo $DxZip $DownloadUrl
+
+tar.exe xf $DxZip -C $BinDir
+
+Remove-Item $DxZip
+
+$CargoBin = "${Home}\.cargo\bin"
+
+if (!(Test-Path $CargoBin)) {
+    New-Item $CargoBin -ItemType Directory | Out-Null
+}
+
+Copy-Item $DxExe "$CargoBin\dx.exe" -Force
+
+# $User = [System.EnvironmentVariableTarget]::User
+# $Path = [System.Environment]::GetEnvironmentVariable('Path', $User)
+# if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) {
+#   [System.Environment]::SetEnvironmentVariable('Path', "${Path};${BinDir}", $User)
+#   $Env:Path += ";${BinDir}"
+# }
+
+Write-Output "dx was installed successfully! 💫"
+Write-Output "Run 'dx --help' to get started"

+ 104 - 0
.github/install.sh

@@ -0,0 +1,104 @@
+#!/bin/sh
+set -eo pipefail
+
+# Reset
+Color_Off=''
+
+# Regular Colors
+Red=''
+Green=''
+Dim='' # White
+
+# Bold
+Bold_White=''
+Bold_Green=''
+
+if [[ -t 1 ]]; then
+    # Reset
+    Color_Off='\033[0m' # Text Reset
+
+    # Regular Colors
+    Red='\033[0;31m'   # Red
+    Green='\033[0;32m' # Green
+    Dim='\033[0;2m'    # White
+
+    # Bold
+    Bold_Green='\033[1;32m' # Bold Green
+    Bold_White='\033[1m'    # Bold White
+fi
+
+error() {
+    echo -e "${Red}error${Color_Off}:" "$@" >&2
+    exit 1
+}
+
+info() {
+    echo -e "${Dim}$@ ${Color_Off}"
+}
+
+info_bold() {
+    echo -e "${Bold_White}$@ ${Color_Off}"
+}
+
+success() {
+    echo -e "${Green}$@ ${Color_Off}"
+}
+
+command -v unzip >/dev/null ||
+    error 'unzip is required to install dx'
+
+if [[ $# -gt 1 ]]; then
+    error 'Too many arguments, only 1 are allowed. The first can be a specific tag of dx to install. (e.g. "dx-v0.7.1")'
+fi
+
+if [ "$OS" = "Windows_NT" ]; then
+	target="x86_64-pc-windows-msvc"
+else
+	case $(uname -sm) in
+	"Darwin x86_64") target="x86_64-apple-darwin" ;;
+	"Darwin arm64") target="aarch64-apple-darwin" ;;
+	"Linux aarch64") target="aarch64-unknown-linux-gnu" ;;
+	*) target="x86_64-unknown-linux-gnu" ;;
+	esac
+fi
+
+GITHUB=${GITHUB-"https://github.com"}
+github_repo="$GITHUB/dioxuslabs/dioxus"
+exe_name=dx
+
+if [[ $# = 0 ]]; then
+    dx_uri=$github_repo/releases/latest/download/dx-$target.zip
+else
+    dx_uri=$github_repo/releases/download/$1/dx-$target.zip
+fi
+
+dx_install="${DX_INSTALL:-$HOME/.dx}"
+bin_dir="$dx_install/bin"
+exe="$bin_dir/dx"
+cargo_bin_dir="$HOME/.cargo/bin"
+cargo_bin_exe="$cargo_bin_dir/dx"
+
+if [ ! -d "$bin_dir" ]; then
+	mkdir -p "$bin_dir"
+fi
+
+curl --fail --location --progress-bar --output "$exe.zip" "$dx_uri"
+if command -v unzip >/dev/null; then
+	unzip -d "$bin_dir" -o "$exe.zip"
+else
+	7z x -o"$bin_dir" -y "$exe.zip"
+fi
+chmod +x "$exe"
+cp "$exe" "$cargo_bin_exe" || error "Failed to copy dx to $cargo_bin_dir"
+rm "$exe.zip"
+echo "  installed: $cargo_bin_exe"
+
+echo
+echo "dx was installed successfully! 💫"
+echo
+
+if command -v dx >/dev/null; then
+	echo "Run 'dx --help' to get started"
+else
+	echo "Run '$exe --help' to get started"
+fi

+ 66 - 25
.github/workflows/main.yml

@@ -45,7 +45,7 @@ jobs:
   check-msrv:
     if: github.event.pull_request.draft == false
     name: Check MSRV
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - uses: dtolnay/rust-toolchain@1.86.0
@@ -66,12 +66,14 @@ jobs:
   test:
     if: github.event.pull_request.draft == false
     name: Test Suite
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
+      - name: Free Disk Space
+        uses: ./.github/actions/free-disk-space
       - uses: awalsh128/cache-apt-pkgs-action@latest
         with:
-          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
           version: 1.0
       - uses: dtolnay/rust-toolchain@1.86.0
         with:
@@ -85,12 +87,14 @@ jobs:
   release-test:
     if: github.event.pull_request.draft == false
     name: Test Suite with Optimizations
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
+      - name: Free Disk Space
+        uses: ./.github/actions/free-disk-space
       - uses: awalsh128/cache-apt-pkgs-action@latest
         with:
-          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
           version: 1.0
       - uses: dtolnay/rust-toolchain@1.86.0
         with:
@@ -104,7 +108,7 @@ jobs:
   fmt:
     if: github.event.pull_request.draft == false
     name: Rustfmt
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - uses: dtolnay/rust-toolchain@1.86.0
@@ -118,12 +122,12 @@ jobs:
   docs:
     if: github.event.pull_request.draft == false
     name: Docs
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - uses: awalsh128/cache-apt-pkgs-action@latest
         with:
-          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
           version: 1.0
       - name: Install Rust ${{ env.rust_nightly }}
         uses: dtolnay/rust-toolchain@nightly
@@ -135,22 +139,18 @@ 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, private items
-        # RUSTDOCFLAGS: --cfg docsrs -Dwarnings
-        #  --document-private-items
+        env:
+          RUSTDOCFLAGS: -Dwarnings --document-private-items
 
   check:
     if: github.event.pull_request.draft == false
     name: Check
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - uses: awalsh128/cache-apt-pkgs-action@latest
         with:
-          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
           version: 1.0
       - uses: dtolnay/rust-toolchain@1.86.0
       - uses: Swatinem/rust-cache@v2
@@ -161,12 +161,12 @@ jobs:
   clippy:
     if: github.event.pull_request.draft == false
     name: Clippy
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - uses: actions/checkout@v4
       - uses: awalsh128/cache-apt-pkgs-action@latest
         with:
-          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
           version: 1.0
       - uses: dtolnay/rust-toolchain@1.86.0
         with:
@@ -181,11 +181,14 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-latest, macos-latest]
+        os: [ubuntu-24.04, macos-latest]
     steps:
       - uses: actions/checkout@v4
-      - uses: DeterminateSystems/nix-installer-action@main
-      - uses: DeterminateSystems/magic-nix-cache-action@main
+      - uses: nixbuild/nix-quick-install-action@master
+      - uses: nix-community/cache-nix-action@main
+        with:
+          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
+          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
       - name: Install omnix
         run: nix --accept-flake-config profile install "github:juspay/omnix"
       - name: Build all flake outputs
@@ -197,6 +200,11 @@ jobs:
     if: github.event.pull_request.draft == false
     name: Playwright Tests
     runs-on: macos-latest
+    strategy:
+      matrix:
+        platform:
+          - { toolchain: 1.86.0 }
+          - { toolchain: beta }
     steps:
       # Do our best to cache the toolchain and node install steps
       - uses: actions/checkout@v4
@@ -206,14 +214,21 @@ jobs:
       - name: Install Rust
         uses: dtolnay/rust-toolchain@master
         with:
-          toolchain: stable
+          toolchain: ${{ matrix.platform.toolchain }}
           targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
       - uses: Swatinem/rust-cache@v2
         with:
+          key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}"
           cache-all-crates: "true"
           cache-on-failure: "true"
+      - name: Wipe dx cache
+        run: |
+          rm -rf ./target/dx
       - name: Playwright
         working-directory: ./packages/playwright-tests
+        env:
+          # The hot patch test requires incremental compilation
+          CARGO_INCREMENTAL: 1
         run: |
           npm ci
           npm install -D @playwright/test
@@ -222,7 +237,7 @@ jobs:
       - uses: actions/upload-artifact@v4
         if: always()
         with:
-          name: playwright-report
+          name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }}
           path: ./packages/playwright-tests/playwright-report/
           retention-days: 30
 
@@ -258,9 +273,25 @@ jobs:
               command: "build",
               args: "--package dioxus-mobile",
             }
+          - {
+              target: x86_64-unknown-linux-gnu,
+              os: ubuntu-24.04,
+              toolchain: "1.86.0",
+              cross: false,
+              command: "build",
+              args: "--all --tests",
+            }
+          - {
+              target: aarch64-unknown-linux-gnu,
+              os: ubuntu-24.04-arm,
+              toolchain: "1.86.0",
+              cross: false,
+              command: "build",
+              args: "--all --tests",
+            }
           - {
               target: aarch64-linux-android,
-              os: ubuntu-latest,
+              os: ubuntu-24.04,
               toolchain: "1.86.0",
               cross: true,
               command: "build",
@@ -280,6 +311,16 @@ jobs:
         if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
         uses: ilammy/setup-nasm@v1
 
+      - name: Free Disk Space
+        if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
+        uses: ./.github/actions/free-disk-space
+
+      - uses: awalsh128/cache-apt-pkgs-action@latest
+        if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
+        with:
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
+          version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
+
       - name: Install cross
         if: ${{ matrix.platform.cross == true }}
         uses: taiki-e/install-action@cross
@@ -332,7 +373,7 @@ jobs:
   # semver:
   #   if: github.event.pull_request.draft == false && !contains(github.event.pull_request.labels.*.name, 'breaking')
   #   name: Semver Check
-  #   runs-on: ubuntu-latest
+  #   runs-on: ubuntu-24.04
   #   steps:
   #     - uses: actions/checkout@v4
   #     - uses: dtolnay/rust-toolchain@1.86.0

+ 1 - 1
.github/workflows/merge.yml

@@ -40,7 +40,7 @@ jobs:
     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
+      - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
       - uses: dtolnay/rust-toolchain@nightly
         with:
           toolchain: nightly-2024-02-01

+ 17 - 7
.github/workflows/publish.yml

@@ -58,9 +58,9 @@ jobs:
           - target: aarch64-apple-darwin
             os: macos-latest
           - target: x86_64-unknown-linux-gnu
-            os: ubuntu-22.04
-          # - target: aarch64-unknown-linux-gnu
-          #   os: ubuntu-latest
+            os: ubuntu-24.04
+          - target: aarch64-unknown-linux-gnu
+            os: ubuntu-24.04-arm
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -73,10 +73,20 @@ jobs:
         if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
         uses: ilammy/setup-nasm@v1
 
+      - name: Free Disk Space
+        if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
+        uses: ./.github/actions/free-disk-space
+
+      - uses: awalsh128/cache-apt-pkgs-action@latest
+        if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
+        with:
+          packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
+          version: 1.0
+
       - name: Install stable
         uses: dtolnay/rust-toolchain@master
         with:
-          toolchain: "1.84.0"
+          toolchain: "1.86.0"
           targets: ${{ matrix.platform.target }}
 
       - uses: Swatinem/rust-cache@v2
@@ -98,11 +108,11 @@ jobs:
           bin: dx
           token: ${{ secrets.GITHUB_TOKEN }}
           target: ${{ matrix.platform.target }}
-          archive: $bin-$target-${{ env.RELEASE_TAG }}
+          archive: $bin-$target
           checksum: sha256
           manifest_path: packages/cli/Cargo.toml
           ref: refs/tags/${{ env.RELEASE_POST }}
-          features: optimizations
+          zip: "all"
 
   # todo: these things
   # Run benchmarks, which we'll use to display on the website
@@ -117,7 +127,7 @@ jobs:
   #     - uses: actions/checkout@v4
   #       ref: ${{ github.event.inputs.channel }}
   #     - run: sudo apt-get update
-  #     - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
+  #     - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
   #     - uses: dtolnay/rust-toolchain@nightly
   #       with:
   #         toolchain: nightly-2024-02-01

+ 2 - 0
.gitignore

@@ -1,8 +1,10 @@
 .dioxus
 /target
+/packages/playwright-tests/cli-optimization/monaco-editor-0.52.2
 /packages/playwright-tests/web/dist
 /packages/playwright-tests/fullstack/dist
 /packages/playwright-tests/test-results
+/packages/playwright-tests/web-hot-patch-temp
 /dist
 .DS_Store
 /examples/assets/test_video.mp4

+ 4 - 1
.vscode/settings.json

@@ -9,6 +9,9 @@
   "[javascript]": {
     "editor.formatOnSave": false
   },
+  "[html]": {
+    "editor.formatOnSave": false
+  },
   "dioxus.formatOnSave": "disabled",
   // "rust-analyzer.check.workspace": true,
   // "rust-analyzer.check.workspace": false,
@@ -21,4 +24,4 @@
   "rust-analyzer.cargo.extraArgs": [
     "--tests"
   ],
-}
+}

+ 894 - 714
Cargo.lock

@@ -182,17 +182,17 @@ dependencies = [
 
 [[package]]
 name = "ahash"
-version = "0.8.11"
+version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
 dependencies = [
  "cfg-if",
  "const-random",
- "getrandom 0.2.16",
+ "getrandom 0.3.3",
  "once_cell",
  "serde",
  "version_check",
- "zerocopy 0.7.35",
+ "zerocopy",
 ]
 
 [[package]]
@@ -253,7 +253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
 dependencies = [
  "android-properties",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cc",
  "cesu8",
  "jni",
@@ -358,12 +358,12 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
 dependencies = [
  "anstyle",
- "once_cell",
+ "once_cell_polyfill",
  "windows-sys 0.59.0",
 ]
 
@@ -379,6 +379,48 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
 
+[[package]]
+name = "anyrender"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5e27059c655004e0f54b4f41f9a42dd607dde74e1cc5186eff4f32bc169783d"
+dependencies = [
+ "kurbo",
+ "peniko",
+ "raw-window-handle 0.6.2",
+]
+
+[[package]]
+name = "anyrender_svg"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3414003d42ba672258f3f83d0c92c6e97ee617d06f6b9ec6f19b895eb29a24a"
+dependencies = [
+ "anyrender",
+ "image",
+ "kurbo",
+ "peniko",
+ "thiserror 2.0.12",
+ "usvg",
+]
+
+[[package]]
+name = "anyrender_vello"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21c4f6e7781da69eab51dd170301963a60eb443b3c23b2f5c5b01094fde65bed"
+dependencies = [
+ "anyrender",
+ "futures-intrusive",
+ "kurbo",
+ "peniko",
+ "pollster",
+ "raw-window-handle 0.6.2",
+ "rustc-hash 1.1.0",
+ "vello",
+ "wgpu 24.0.5",
+]
+
 [[package]]
 name = "app_units"
 version = "0.7.8"
@@ -413,7 +455,7 @@ dependencies = [
  "apple-xar",
  "base64 0.21.7",
  "bcder",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bytes",
  "chrono",
  "clap",
@@ -612,7 +654,7 @@ version = "0.38.0+1.3.281"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
 dependencies = [
- "libloading 0.8.6",
+ "libloading 0.8.7",
 ]
 
 [[package]]
@@ -630,14 +672,14 @@ dependencies = [
  "serde_repr",
  "tokio",
  "url",
- "zbus 5.6.0",
+ "zbus 5.7.1",
 ]
 
 [[package]]
 name = "askama_escape"
-version = "0.10.3"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4"
 
 [[package]]
 name = "asn1-rs"
@@ -783,9 +825,9 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.4.0"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
 dependencies = [
  "async-lock",
  "cfg-if",
@@ -794,7 +836,7 @@ dependencies = [
  "futures-lite",
  "parking",
  "polling",
- "rustix 0.38.44",
+ "rustix 1.0.7",
  "slab",
  "tracing",
  "windows-sys 0.59.0",
@@ -982,9 +1024,6 @@ name = "atomic_refcell"
 version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "atspi"
@@ -1039,9 +1078,9 @@ dependencies = [
 
 [[package]]
 name = "auth-git2"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d55eead120c93036f531829cf9b85830a474e75ce71169680879d28078321ddc"
+checksum = "4888bf91cce63baf1670512d0f12b5d636179a4abbad6504812ac8ab124b3efe"
 dependencies = [
  "dirs 6.0.0",
  "git2",
@@ -1067,9 +1106,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
 [[package]]
 name = "av1-grain"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8"
 dependencies = [
  "anyhow",
  "arrayvec",
@@ -1090,9 +1129,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.13.0"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878"
+checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
 dependencies = [
  "aws-lc-sys",
  "untrusted 0.7.1",
@@ -1101,9 +1140,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.28.2"
+version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
+checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
 dependencies = [
  "bindgen",
  "cc",
@@ -1438,7 +1477,7 @@ version = "0.69.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cexpr",
  "clang-sys",
  "itertools 0.12.1",
@@ -1505,9 +1544,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.9.0"
+version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 dependencies = [
  "serde",
 ]
@@ -1563,27 +1602,29 @@ dependencies = [
 
 [[package]]
 name = "blitz-dom"
-version = "0.1.0-alpha.1"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eae8889dde80e2aaceec27455c63446ca841cb7217a4f5489691295da922792b"
+checksum = "6d0f5c53216da39251e7039c99322c03ac16f23b1b68d5d5ece03f5bb92f139b"
 dependencies = [
  "accesskit",
  "app_units",
  "atomic_refcell",
+ "bitflags 2.9.1",
  "blitz-traits",
- "color 0.2.3",
+ "color",
  "cursor-icon",
  "euclid",
  "html-escape",
  "image",
  "keyboard-types",
- "markup5ever 0.15.0",
+ "markup5ever 0.16.2",
+ "objc2 0.6.1",
  "parley",
- "peniko 0.3.2",
- "selectors 0.27.0",
+ "peniko",
+ "percent-encoding",
+ "selectors 0.29.0",
  "slab",
  "smallvec",
- "string_cache",
  "stylo",
  "stylo_config",
  "stylo_dom",
@@ -1597,68 +1638,67 @@ dependencies = [
 
 [[package]]
 name = "blitz-net"
-version = "0.1.0-alpha.1"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efc80023b8f6758d7f61bda68440917b72cbe2fe6187cbe1b7837e73b2522dfe"
+checksum = "c17d660462f6ebf8045f94045586a6ec9c46d4d0b4dcf8bb09390b3b4dc956e7"
 dependencies = [
  "blitz-traits",
  "data-url 0.3.1",
  "reqwest 0.12.15",
- "thiserror 1.0.69",
  "tokio",
 ]
 
 [[package]]
-name = "blitz-renderer-vello"
-version = "0.1.0-alpha.1"
+name = "blitz-paint"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fdafaf87eb502da99667fad2ae9757d01c8bd83772f6b7b6a78c48c3ab6ff5"
+checksum = "91e1eb842e5620c41f3059f35d7365d892247d024785b319787e5a1a8b46c247"
 dependencies = [
+ "anyrender",
+ "anyrender_svg",
  "blitz-dom",
  "blitz-traits",
- "color 0.2.3",
+ "color",
  "euclid",
- "futures-intrusive",
- "image",
+ "kurbo",
  "parley",
- "pollster",
- "raw-window-handle 0.6.2",
+ "peniko",
  "stylo",
  "taffy",
- "vello",
- "vello_svg",
- "wgpu 23.0.1",
+ "tracing",
+ "usvg",
 ]
 
 [[package]]
 name = "blitz-shell"
-version = "0.1.0-alpha.1"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef0a6122f3619a746dddd21883edaae5f791d6e9f401dbdf7f7756caa4f69453"
+checksum = "7efc5fc3258cac0b4fa24fa2610e88980acb6526c66bfc9a79355ec042541d1b"
 dependencies = [
  "accesskit",
  "accesskit_winit",
  "android-activity",
+ "anyrender",
  "blitz-dom",
+ "blitz-paint",
  "blitz-traits",
  "futures-util",
  "keyboard-types",
- "muda 0.11.5",
  "tracing",
  "winit",
 ]
 
 [[package]]
 name = "blitz-traits"
-version = "0.1.0-alpha.1"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9972442314e8bf9dd65af7aee0634a2d575972587d404520c7c6986abbd523d3"
+checksum = "e17bc003ba7744d19ef07ff86a2214d46d761caef0026676fc7216c5242fc284"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bytes",
+ "cursor-icon",
  "http 1.3.1",
  "keyboard-types",
- "raw-window-handle 0.6.2",
  "smol_str",
  "url",
 ]
@@ -1799,7 +1839,7 @@ version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdf0ca73de70c3da94e4194e4a01fe732378f55d47cf4c0588caab22a0dbfa14"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
  "chrono",
  "either",
  "indexmap 2.9.0",
@@ -1813,11 +1853,11 @@ dependencies = [
 
 [[package]]
 name = "browserslist-rs"
-version = "0.17.0"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74c973b79d9b6b89854493185ab760c6ef8e54bcfad10ad4e33991e46b374ac8"
+checksum = "2f95aff901882c66e4b642f3f788ceee152ef44f8a5ef12cb1ddee5479c483be"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
  "chrono",
  "either",
  "indexmap 2.9.0",
@@ -1958,7 +1998,7 @@ version = "0.18.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cairo-sys-rs",
  "glib",
  "libc",
@@ -1983,7 +2023,7 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "log",
  "polling",
  "rustix 0.38.44",
@@ -2022,6 +2062,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cargo-config2"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dc3749a36e0423c991f1e7a3e4ab0c36a1f489658313db4b187d401d79cc461"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "toml_edit 0.22.26",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "cargo-generate"
 version = "0.23.3"
@@ -2075,9 +2127,9 @@ dependencies = [
 
 [[package]]
 name = "cargo-util-schemas"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e788664537bc508c6f252ca8b0e64275d89ca3ce11aeb71452a3554f390e3a65"
+checksum = "ea8b01266e95c3cf839fe626e651fa36a9171033caa917a773d7a0ba1d5ce6be"
 dependencies = [
  "semver",
  "serde",
@@ -2154,9 +2206,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.21"
+version = "1.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
+checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
 dependencies = [
  "jobserver",
  "libc",
@@ -2295,14 +2347,14 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
 dependencies = [
  "glob",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
 ]
 
 [[package]]
 name = "clap"
-version = "4.5.37"
+version = "4.5.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
+checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -2310,9 +2362,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.37"
+version = "4.5.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
+checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
 dependencies = [
  "anstream",
  "anstyle",
@@ -2361,29 +2413,13 @@ dependencies = [
 
 [[package]]
 name = "cocoa"
-version = "0.25.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
-dependencies = [
- "bitflags 1.3.2",
- "block",
- "cocoa-foundation 0.1.2",
- "core-foundation 0.9.4",
- "core-graphics 0.23.2",
- "foreign-types 0.5.0",
- "libc",
- "objc",
-]
-
-[[package]]
-name = "cocoa"
-version = "0.26.0"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
+checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
- "cocoa-foundation 0.2.0",
+ "cocoa-foundation",
  "core-foundation 0.10.0",
  "core-graphics 0.24.0",
  "foreign-types 0.5.0",
@@ -2393,29 +2429,14 @@ dependencies = [
 
 [[package]]
 name = "cocoa-foundation"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
-dependencies = [
- "bitflags 1.3.2",
- "block",
- "core-foundation 0.9.4",
- "core-graphics-types 0.1.3",
- "libc",
- "objc",
-]
-
-[[package]]
-name = "cocoa-foundation"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
+checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
  "core-foundation 0.10.0",
  "core-graphics-types 0.2.0",
- "libc",
  "objc",
 ]
 
@@ -2448,15 +2469,9 @@ dependencies = [
 
 [[package]]
 name = "color"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c387f6cef110ee8eaf12fca5586d3d303c07c594f4a5f02c768b6470b70dbd"
-
-[[package]]
-name = "color"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "010263546cea9f9f8385a5b7aad534b9e6448e62a0d3bf9da29d583308dd11bb"
+checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698"
 
 [[package]]
 name = "color_quant"
@@ -2624,7 +2639,7 @@ dependencies = [
 
 [[package]]
 name = "const-serialize"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "const-serialize",
  "const-serialize-macro",
@@ -2634,7 +2649,7 @@ dependencies = [
 
 [[package]]
 name = "const-serialize-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2795,7 +2810,7 @@ version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.10.0",
  "core-graphics-types 0.2.0",
  "foreign-types 0.5.0",
@@ -2819,7 +2834,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.10.0",
  "libc",
 ]
@@ -2943,6 +2958,14 @@ dependencies = [
  "itertools 0.10.5",
 ]
 
+[[package]]
+name = "cross-tls-crate"
+version = "0.1.0"
+
+[[package]]
+name = "cross-tls-crate-dylib"
+version = "0.1.0"
+
 [[package]]
 name = "crossbeam"
 version = "0.8.4"
@@ -3005,7 +3028,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "crossterm_winapi",
  "mio",
  "parking_lot",
@@ -3021,7 +3044,7 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "crossterm_winapi",
  "derive_more 2.0.1",
  "document-features",
@@ -3173,19 +3196,19 @@ dependencies = [
 
 [[package]]
 name = "ctrlc"
-version = "3.4.6"
+version = "3.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
+checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
 dependencies = [
- "nix",
+ "nix 0.30.1",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "cursor-icon"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
 
 [[package]]
 name = "curve25519-dalek"
@@ -3289,8 +3312,8 @@ version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
 dependencies = [
- "bitflags 2.9.0",
- "libloading 0.8.6",
+ "bitflags 2.9.1",
+ "libloading 0.8.7",
  "winapi",
 ]
 
@@ -3398,7 +3421,7 @@ dependencies = [
 
 [[package]]
 name = "depinfo"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "thiserror 2.0.12",
 ]
@@ -3569,7 +3592,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "criterion",
  "dioxus",
@@ -3605,12 +3628,12 @@ dependencies = [
  "tokio",
  "tracing",
  "warnings",
- "wasm-split",
+ "wasm-splitter",
 ]
 
 [[package]]
 name = "dioxus-asset-resolver"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-cli-config",
  "http 1.3.1",
@@ -3619,13 +3642,13 @@ dependencies = [
  "ndk",
  "ndk-context",
  "ndk-sys 0.6.0+11769913",
+ "percent-encoding",
  "thiserror 2.0.12",
- "urlencoding",
 ]
 
 [[package]]
 name = "dioxus-autofmt"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-rsx",
  "pretty_assertions",
@@ -3638,7 +3661,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-check"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "indoc",
  "owo-colors",
@@ -3650,7 +3673,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-cli"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "ansi-to-html",
  "ansi-to-tui",
@@ -3662,6 +3685,7 @@ dependencies = [
  "backtrace",
  "brotli 7.0.0",
  "built",
+ "cargo-config2",
  "cargo-generate",
  "cargo_metadata",
  "cargo_toml",
@@ -3699,7 +3723,7 @@ dependencies = [
  "headers",
  "html_parser",
  "hyper 1.6.0",
- "hyper-rustls 0.27.5",
+ "hyper-rustls 0.27.6",
  "hyper-util",
  "ignore",
  "include_dir",
@@ -3713,7 +3737,6 @@ dependencies = [
  "memoize",
  "notify",
  "object 0.36.7",
- "once_cell",
  "open",
  "path-absolutize",
  "pdb",
@@ -3725,6 +3748,8 @@ dependencies = [
  "regex",
  "reqwest 0.12.15",
  "rustls 0.23.27",
+ "self-replace",
+ "self_update",
  "serde",
  "serde_json",
  "shell-words",
@@ -3753,7 +3778,7 @@ dependencies = [
  "walkdir",
  "walrus",
  "wasm-bindgen-externref-xform",
- "wasm-encoder 0.228.0",
+ "wasm-encoder 0.229.0",
  "wasm-opt",
  "wasm-split-cli",
  "wasmparser 0.226.0",
@@ -3762,17 +3787,18 @@ dependencies = [
 
 [[package]]
 name = "dioxus-cli-config"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "wasm-bindgen",
 ]
 
 [[package]]
 name = "dioxus-cli-opt"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "anyhow",
  "browserslist-rs 0.16.0",
+ "built",
  "codemap",
  "const-serialize",
  "grass",
@@ -3828,7 +3854,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-config-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3836,11 +3862,11 @@ dependencies = [
 
 [[package]]
 name = "dioxus-config-macros"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 
 [[package]]
 name = "dioxus-core"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "const_format",
  "dioxus",
@@ -3871,7 +3897,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-core-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "convert_case 0.8.0",
  "dioxus",
@@ -3887,18 +3913,15 @@ dependencies = [
 
 [[package]]
 name = "dioxus-core-types"
-version = "0.6.3"
-dependencies = [
- "once_cell",
-]
+version = "0.7.0-alpha.1"
 
 [[package]]
 name = "dioxus-desktop"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
- "cocoa 0.26.0",
+ "cocoa",
  "core-foundation 0.10.0",
  "dioxus",
  "dioxus-asset-resolver",
@@ -3922,13 +3945,15 @@ dependencies = [
  "infer",
  "jni",
  "lazy-js-bundle",
- "muda 0.16.1",
+ "libc",
+ "muda",
  "ndk",
  "ndk-context",
  "ndk-sys 0.6.0+11769913",
  "objc",
  "objc_id",
- "once_cell",
+ "open",
+ "percent-encoding",
  "rand 0.8.5",
  "reqwest 0.12.15",
  "rfd",
@@ -3944,14 +3969,12 @@ dependencies = [
  "tokio-tungstenite",
  "tracing",
  "tray-icon",
- "urlencoding",
- "webbrowser",
  "wry",
 ]
 
 [[package]]
 name = "dioxus-devtools"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-cli-config",
  "dioxus-core",
@@ -3969,7 +3992,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-devtools-types"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-core",
  "serde",
@@ -3978,7 +4001,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-document"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -3996,7 +4019,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-dx-wire-format"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "cargo_metadata",
  "serde",
@@ -4005,7 +4028,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-examples"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-std",
  "base64 0.22.1",
@@ -4024,14 +4047,14 @@ dependencies = [
  "serde",
  "serde_json",
  "tokio",
- "wasm-split",
+ "wasm-splitter",
  "web-time",
  "wgpu 0.19.4",
 ]
 
 [[package]]
 name = "dioxus-ext"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-autofmt",
  "dioxus-rsx-rosetta",
@@ -4042,7 +4065,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-trait",
  "aws-lc-rs",
@@ -4068,8 +4091,7 @@ dependencies = [
  "futures-channel",
  "futures-util",
  "generational-box",
- "hyper-rustls 0.27.5",
- "once_cell",
+ "hyper-rustls 0.27.6",
  "parking_lot",
  "pin-project",
  "rustls 0.23.27",
@@ -4088,12 +4110,13 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack-hooks"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-core",
  "dioxus-fullstack",
  "dioxus-fullstack-protocol",
+ "dioxus-history",
  "dioxus-hooks",
  "dioxus-lib",
  "dioxus-signals",
@@ -4103,7 +4126,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack-protocol"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "base64 0.22.1",
  "ciborium",
@@ -4114,7 +4137,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-history"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4123,7 +4146,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-hooks"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4142,7 +4165,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-html"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-trait",
  "dioxus",
@@ -4171,7 +4194,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-html-internal-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "convert_case 0.8.0",
  "proc-macro2",
@@ -4182,7 +4205,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-interpreter-js"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-core",
  "dioxus-core-types",
@@ -4200,7 +4223,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-isrg"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "chrono",
  "http 1.3.1",
@@ -4213,7 +4236,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-lib"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-config-macro",
@@ -4230,7 +4253,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-liveview"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "axum 0.8.4",
  "dioxus",
@@ -4258,7 +4281,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-logger"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "console_error_panic_hook",
  "dioxus",
@@ -4270,23 +4293,24 @@ dependencies = [
 
 [[package]]
 name = "dioxus-mobile"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-cli-config",
  "dioxus-desktop",
  "dioxus-lib",
  "jni",
  "libc",
- "once_cell",
 ]
 
 [[package]]
 name = "dioxus-native"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
+ "anyrender",
+ "anyrender_vello",
  "blitz-dom",
  "blitz-net",
- "blitz-renderer-vello",
+ "blitz-paint",
  "blitz-shell",
  "blitz-traits",
  "dioxus-asset-resolver",
@@ -4334,6 +4358,7 @@ name = "dioxus-playwright-fullstack-test"
 version = "0.1.0"
 dependencies = [
  "dioxus",
+ "futures",
  "serde",
  "tokio",
 ]
@@ -4348,6 +4373,20 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "dioxus-playwright-web-hash-routing-test"
+version = "0.0.1"
+dependencies = [
+ "dioxus",
+]
+
+[[package]]
+name = "dioxus-playwright-web-routing-test"
+version = "0.0.1"
+dependencies = [
+ "dioxus",
+]
+
 [[package]]
 name = "dioxus-playwright-web-test"
 version = "0.0.1"
@@ -4369,7 +4408,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-router"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "axum 0.8.4",
  "base64 0.22.1",
@@ -4380,19 +4419,20 @@ dependencies = [
  "dioxus-fullstack-hooks",
  "dioxus-history",
  "dioxus-lib",
+ "dioxus-router",
  "dioxus-router-macro",
  "dioxus-ssr",
+ "percent-encoding",
  "rustversion",
  "serde",
  "tokio",
  "tracing",
  "url",
- "urlencoding",
 ]
 
 [[package]]
 name = "dioxus-router-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "base16",
  "digest",
@@ -4406,7 +4446,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "prettier-please",
  "prettyplease",
@@ -4418,7 +4458,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx-hotreload"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus-core",
  "dioxus-core-types",
@@ -4433,7 +4473,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx-rosetta"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "convert_case 0.8.0",
  "dioxus-autofmt",
@@ -4449,7 +4489,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-server"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-trait",
  "aws-lc-rs",
@@ -4469,15 +4509,15 @@ dependencies = [
  "dioxus-lib",
  "dioxus-router",
  "dioxus-ssr",
+ "enumset",
  "futures-channel",
  "futures-util",
  "generational-box",
  "http 1.3.1",
  "hyper 1.6.0",
- "hyper-rustls 0.27.5",
+ "hyper-rustls 0.27.6",
  "hyper-util",
  "inventory",
- "once_cell",
  "parking_lot",
  "pin-project",
  "rustls 0.23.27",
@@ -4498,14 +4538,13 @@ dependencies = [
 
 [[package]]
 name = "dioxus-signals"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dioxus",
  "dioxus-core",
  "futures-channel",
  "futures-util",
  "generational-box",
- "once_cell",
  "parking_lot",
  "rand 0.8.5",
  "reqwest 0.12.15",
@@ -4519,7 +4558,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-ssr"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "askama_escape",
  "dioxus",
@@ -4538,7 +4577,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-web"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-trait",
  "ciborium",
@@ -4576,7 +4615,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus_server_macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "axum 0.8.4",
  "dioxus",
@@ -4653,7 +4692,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.6.1",
  "libc",
  "objc2 0.6.1",
@@ -4665,7 +4704,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "objc2 0.6.1",
 ]
 
@@ -4692,7 +4731,7 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
 dependencies = [
- "libloading 0.8.6",
+ "libloading 0.8.7",
 ]
 
 [[package]]
@@ -4845,6 +4884,7 @@ dependencies = [
  "ed25519",
  "serde",
  "sha2",
+ "signature",
  "subtle",
  "zeroize",
 ]
@@ -5035,9 +5075,9 @@ dependencies = [
 
 [[package]]
 name = "errno"
-version = "0.3.11"
+version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -5320,9 +5360,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
 
 [[package]]
 name = "font-types"
-version = "0.8.4"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
+checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
 dependencies = [
  "bytemuck",
 ]
@@ -5339,42 +5379,43 @@ dependencies = [
 
 [[package]]
 name = "fontconfig-parser"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7"
+checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
 dependencies = [
  "roxmltree",
 ]
 
 [[package]]
 name = "fontdb"
-version = "0.22.0"
+version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716"
+checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
 dependencies = [
  "fontconfig-parser",
  "log",
  "memmap2",
  "slotmap",
  "tinyvec",
- "ttf-parser 0.24.1",
+ "ttf-parser",
 ]
 
 [[package]]
 name = "fontique"
-version = "0.3.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b5fcb214137f01bc842c4fd633236255c51f8a24c6d3923eb8361c6d0940737"
+checksum = "39f97079e1293b8c1e9fb03a2875d328bd2ee8f3b95ce62959c0acc04049c708"
 dependencies = [
  "bytemuck",
  "fontconfig-cache-parser",
  "hashbrown 0.15.3",
  "icu_locid",
  "memmap2",
+ "objc2 0.6.1",
  "objc2-core-foundation",
  "objc2-core-text",
  "objc2-foundation 0.3.1",
- "peniko 0.3.2",
+ "peniko",
  "read-fonts",
  "roxmltree",
  "smallvec",
@@ -5474,7 +5515,7 @@ dependencies = [
  "cfg-if",
  "cvt",
  "libc",
- "nix",
+ "nix 0.29.0",
  "windows-sys 0.52.0",
 ]
 
@@ -5560,11 +5601,19 @@ dependencies = [
  "dioxus",
  "futures",
  "futures-util",
- "once_cell",
  "serde",
  "tokio",
 ]
 
+[[package]]
+name = "fullstack-websocket-example"
+version = "0.1.0"
+dependencies = [
+ "dioxus",
+ "futures",
+ "tokio",
+]
+
 [[package]]
 name = "funty"
 version = "2.0.0"
@@ -5804,7 +5853,7 @@ dependencies = [
 
 [[package]]
 name = "generational-box"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "criterion",
  "parking_lot",
@@ -5868,9 +5917,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -5955,7 +6004,7 @@ version = "0.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "libc",
  "libgit2-sys",
  "log",
@@ -6005,7 +6054,7 @@ version = "0.14.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8dc2c844c4cf141884678cabef736fd91dd73068b9146e6f004ba1a0457944b6"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bstr",
  "gix-path",
  "libc",
@@ -6084,7 +6133,7 @@ version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "20972499c03473e773a2099e5fd0c695b9b72465837797a51a43391a1635a030"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bstr",
  "gix-features 0.41.1",
  "gix-path",
@@ -6159,9 +6208,9 @@ dependencies = [
 
 [[package]]
 name = "gix-path"
-version = "0.10.17"
+version = "0.10.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c091d2e887e02c3462f52252c5ea61150270c0f2657b642e8d0d6df56c16e642"
+checksum = "567f65fec4ef10dfab97ae71f26a27fd4d7fe7b8e3f90c8a58551c41ff3fb65b"
 dependencies = [
  "bstr",
  "gix-trace",
@@ -6198,7 +6247,7 @@ version = "0.10.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "47aeb0f13de9ef2f3033f5ff218de30f44db827ac9f1286f9ef050aacddd5888"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "gix-path",
  "libc",
  "windows-sys 0.52.0",
@@ -6280,7 +6329,7 @@ version = "0.18.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "futures-channel",
  "futures-core",
  "futures-executor",
@@ -6329,9 +6378,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 
 [[package]]
 name = "global-hotkey"
-version = "0.6.4"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271"
+checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
 dependencies = [
  "crossbeam-channel",
  "keyboard-types",
@@ -6340,7 +6389,8 @@ dependencies = [
  "once_cell",
  "thiserror 2.0.12",
  "windows-sys 0.59.0",
- "x11-dl",
+ "x11rb",
+ "xkeysym",
 ]
 
 [[package]]
@@ -6426,9 +6476,9 @@ dependencies = [
 
 [[package]]
 name = "glow"
-version = "0.14.2"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483"
+checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
 dependencies = [
  "js-sys",
  "slotmap",
@@ -6482,7 +6532,7 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "gpu-alloc-types",
 ]
 
@@ -6492,7 +6542,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -6526,18 +6576,18 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "gpu-descriptor-types 0.1.2",
  "hashbrown 0.14.5",
 ]
 
 [[package]]
 name = "gpu-descriptor"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
+checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "gpu-descriptor-types 0.2.0",
  "hashbrown 0.15.3",
 ]
@@ -6548,7 +6598,7 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -6557,7 +6607,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -6752,7 +6802,7 @@ version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
 ]
 
 [[package]]
@@ -6761,7 +6811,7 @@ version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
  "allocator-api2",
  "serde",
 ]
@@ -6793,10 +6843,10 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "com",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "thiserror 1.0.69",
  "widestring",
  "winapi",
@@ -6867,12 +6917,6 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
-
 [[package]]
 name = "hermit-abi"
 version = "0.5.1"
@@ -7128,11 +7172,10 @@ dependencies = [
 
 [[package]]
 name = "hyper-rustls"
-version = "0.27.5"
+version = "0.27.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
+checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
 dependencies = [
- "futures-util",
  "http 1.3.1",
  "hyper 1.6.0",
  "hyper-util",
@@ -7143,7 +7186,7 @@ dependencies = [
  "tokio",
  "tokio-rustls 0.26.2",
  "tower-service",
- "webpki-roots 0.26.11",
+ "webpki-roots 1.0.0",
 ]
 
 [[package]]
@@ -7177,9 +7220,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
+checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -7207,7 +7250,7 @@ dependencies = [
  "js-sys",
  "log",
  "wasm-bindgen",
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
 ]
 
 [[package]]
@@ -7226,88 +7269,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
 dependencies = [
  "displaydoc",
- "yoke",
+ "yoke 0.7.5",
  "zerofrom",
- "zerovec",
+ "zerovec 0.10.4",
 ]
 
 [[package]]
-name = "icu_locid"
-version = "1.5.0"
+name = "icu_collections"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
 dependencies = [
  "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
+ "potential_utf",
+ "yoke 0.8.0",
+ "zerofrom",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
+name = "icu_locale_core"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
 dependencies = [
  "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
+ "litemap 0.8.0",
+ "tinystr 0.8.1",
+ "writeable 0.6.1",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
-name = "icu_locid_transform_data"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
-
-[[package]]
-name = "icu_normalizer"
+name = "icu_locid"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
 dependencies = [
  "displaydoc",
- "icu_collections",
+ "litemap 0.7.5",
+ "tinystr 0.7.6",
+ "writeable 0.5.5",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections 2.0.0",
  "icu_normalizer_data",
  "icu_properties",
- "icu_provider",
+ "icu_provider 2.0.0",
  "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
 name = "icu_normalizer_data"
-version = "1.5.1"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
 
 [[package]]
 name = "icu_properties"
-version = "1.5.1"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
 dependencies = [
  "displaydoc",
- "icu_collections",
- "icu_locid_transform",
+ "icu_collections 2.0.0",
+ "icu_locale_core",
  "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
+ "icu_provider 2.0.0",
+ "potential_utf",
+ "zerotrie",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
 name = "icu_properties_data"
-version = "1.5.1"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
 
 [[package]]
 name = "icu_provider"
@@ -7319,11 +7365,28 @@ dependencies = [
  "icu_locid",
  "icu_provider_macros",
  "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
+ "tinystr 0.7.6",
+ "writeable 0.5.5",
+ "yoke 0.7.5",
+ "zerofrom",
+ "zerovec 0.10.4",
+]
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr 0.8.1",
+ "writeable 0.6.1",
+ "yoke 0.8.0",
  "zerofrom",
- "zerovec",
+ "zerotrie",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
@@ -7345,12 +7408,12 @@ checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de"
 dependencies = [
  "core_maths",
  "displaydoc",
- "icu_collections",
+ "icu_collections 1.5.0",
  "icu_locid",
- "icu_provider",
+ "icu_provider 1.5.0",
  "icu_segmenter_data",
  "utf8_iter",
- "zerovec",
+ "zerovec 0.10.4",
 ]
 
 [[package]]
@@ -7396,9 +7459,9 @@ dependencies = [
 
 [[package]]
 name = "idna_adapter"
-version = "1.2.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
 dependencies = [
  "icu_normalizer",
  "icu_properties",
@@ -7574,7 +7637,7 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "inotify-sys",
  "libc",
 ]
@@ -7825,9 +7888,9 @@ dependencies = [
 
 [[package]]
 name = "jiff"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
+checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
 dependencies = [
  "jiff-static",
  "jiff-tzdb-platform",
@@ -7840,9 +7903,9 @@ dependencies = [
 
 [[package]]
 name = "jiff-static"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
+checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7892,7 +7955,7 @@ version = "0.1.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
 dependencies = [
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
  "libc",
 ]
 
@@ -7979,7 +8042,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "serde",
  "unicode-segmentation",
 ]
@@ -7991,7 +8054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
 dependencies = [
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "pkg-config",
 ]
 
@@ -8108,7 +8171,7 @@ dependencies = [
 
 [[package]]
 name = "lazy-js-bundle"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 
 [[package]]
 name = "lazy_static"
@@ -8209,12 +8272,12 @@ dependencies = [
 
 [[package]]
 name = "libloading"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.6",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
@@ -8229,7 +8292,7 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "libc",
  "redox_syscall 0.5.12",
 ]
@@ -8292,13 +8355,13 @@ dependencies = [
 
 [[package]]
 name = "lightningcss"
-version = "1.0.0-alpha.65"
+version = "1.0.0-alpha.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c84f971730745f4aaac013b6cf4328baf1548efc973c0d95cfd843a3c1ca07af"
+checksum = "9a73ffa17de66534e4b527232f44aa0a89fad22c4f4e0735f9be35494f058e54"
 dependencies = [
- "ahash 0.8.11",
- "bitflags 2.9.0",
- "browserslist-rs 0.17.0",
+ "ahash 0.8.12",
+ "bitflags 2.9.1",
+ "browserslist-rs 0.18.1",
  "const-str 0.3.2",
  "cssparser 0.33.0",
  "cssparser-color",
@@ -8412,6 +8475,12 @@ version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
 
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
 [[package]]
 name = "litrs"
 version = "0.4.1"
@@ -8500,6 +8569,12 @@ dependencies = [
  "hashbrown 0.15.3",
 ]
 
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
 [[package]]
 name = "lzma-sys"
 version = "0.1.20"
@@ -8523,7 +8598,7 @@ version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
 dependencies = [
- "nix",
+ "nix 0.29.0",
  "winapi",
 ]
 
@@ -8560,7 +8635,7 @@ dependencies = [
 
 [[package]]
 name = "manganis"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "const-serialize",
  "manganis-core",
@@ -8569,7 +8644,7 @@ dependencies = [
 
 [[package]]
 name = "manganis-core"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "const-serialize",
  "dioxus",
@@ -8581,7 +8656,7 @@ dependencies = [
 
 [[package]]
 name = "manganis-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "dunce",
  "macro-string",
@@ -8608,16 +8683,13 @@ dependencies = [
 
 [[package]]
 name = "markup5ever"
-version = "0.15.0"
+version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a7b81dfb91586d0677086d40a6d755070e0799b71bb897485bac408dfd5c69"
+checksum = "2e4cd8c02f18a011991a039855480c64d74291c5792fcc160d55d77dc4de4a39"
 dependencies = [
  "log",
- "phf 0.11.3",
- "phf_codegen 0.11.3",
- "string_cache",
- "string_cache_codegen",
  "tendril",
+ "web_atoms",
 ]
 
 [[package]]
@@ -8745,7 +8817,7 @@ version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
  "core-graphics-types 0.1.3",
  "foreign-types 0.5.0",
@@ -8756,11 +8828,11 @@ dependencies = [
 
 [[package]]
 name = "metal"
-version = "0.29.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
+checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
  "core-graphics-types 0.1.3",
  "foreign-types 0.5.0",
@@ -8833,14 +8905,14 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 dependencies = [
  "libc",
  "log",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -8868,24 +8940,6 @@ dependencies = [
  "nasm-rs",
 ]
 
-[[package]]
-name = "muda"
-version = "0.11.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453"
-dependencies = [
- "cocoa 0.25.0",
- "crossbeam-channel",
- "gtk",
- "keyboard-types",
- "objc",
- "once_cell",
- "png",
- "serde",
- "thiserror 1.0.69",
- "windows-sys 0.52.0",
-]
-
 [[package]]
 name = "muda"
 version = "0.16.1"
@@ -8931,7 +8985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843"
 dependencies = [
  "bit-set 0.5.3",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "codespan-reporting 0.11.1",
  "hexf-parse",
  "indexmap 2.9.0",
@@ -8946,22 +9000,23 @@ dependencies = [
 
 [[package]]
 name = "naga"
-version = "23.1.0"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
+checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e"
 dependencies = [
  "arrayvec",
  "bit-set 0.8.0",
- "bitflags 2.9.0",
- "cfg_aliases 0.1.1",
+ "bitflags 2.9.1",
+ "cfg_aliases 0.2.1",
  "codespan-reporting 0.11.1",
  "hexf-parse",
  "indexmap 2.9.0",
  "log",
  "rustc-hash 1.1.0",
  "spirv",
+ "strum 0.26.3",
  "termcolor",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "unicode-xid",
 ]
 
@@ -9006,7 +9061,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "jni-sys",
  "log",
  "ndk-sys 0.6.0+11769913",
@@ -9085,7 +9140,20 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
+ "cfg-if",
+ "cfg_aliases 0.2.1",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.9.1",
  "cfg-if",
  "cfg_aliases 0.2.1",
  "libc",
@@ -9144,7 +9212,7 @@ version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "filetime",
  "fsevent-sys",
  "inotify",
@@ -9388,7 +9456,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "libc",
  "objc2 0.5.2",
@@ -9404,7 +9472,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.6.1",
  "objc2 0.6.1",
  "objc2-core-foundation",
@@ -9417,7 +9485,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-core-location",
@@ -9441,7 +9509,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -9453,7 +9521,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "dispatch2 0.3.0",
  "objc2 0.6.1",
 ]
@@ -9464,7 +9532,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "objc2-core-foundation",
 ]
 
@@ -9498,7 +9566,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ba833d4a1cb1aac330f8c973fd92b6ff1858e4aef5cdd00a255eefb28022fb5"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "objc2-core-foundation",
 ]
 
@@ -9514,7 +9582,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "dispatch",
  "libc",
@@ -9527,7 +9595,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.6.1",
  "objc2 0.6.1",
  "objc2-core-foundation",
@@ -9551,7 +9619,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -9563,7 +9631,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -9586,7 +9654,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-cloud-kit",
@@ -9618,7 +9686,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "objc2 0.5.2",
  "objc2-core-location",
@@ -9712,6 +9780,12 @@ dependencies = [
  "portable-atomic",
 ]
 
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
 [[package]]
 name = "oorandom"
 version = "11.1.5"
@@ -9741,7 +9815,7 @@ version = "0.10.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cfg-if",
  "foreign-types 0.3.2",
  "libc",
@@ -9813,6 +9887,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "ordered-stream"
 version = "0.2.0"
@@ -9825,9 +9908,9 @@ dependencies = [
 
 [[package]]
 name = "os_pipe"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
+checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -9881,14 +9964,14 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
 dependencies = [
- "ttf-parser 0.25.1",
+ "ttf-parser",
 ]
 
 [[package]]
 name = "owo-colors"
-version = "4.2.0"
+version = "4.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564"
+checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
 dependencies = [
  "supports-color 2.1.0",
  "supports-color 3.0.2",
@@ -9976,11 +10059,11 @@ dependencies = [
 
 [[package]]
 name = "parcel_selectors"
-version = "0.28.1"
+version = "0.28.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dccbc6fb560df303a44e511618256029410efbc87779018f751ef12c488271fe"
+checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cssparser 0.33.0",
  "log",
  "phf 0.11.3",
@@ -10036,13 +10119,13 @@ dependencies = [
 
 [[package]]
 name = "parley"
-version = "0.3.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d1c2b33b240c246f06cfceac48dc6c96040cb177d2aa5348899982b298b5577"
+checksum = "13e57638545cf2ba4c3e72cc5715e53b1880b829cc3dbefda3d1700c58efe723"
 dependencies = [
  "fontique",
  "hashbrown 0.15.3",
- "peniko 0.3.2",
+ "peniko",
  "skrifa",
  "swash",
 ]
@@ -10147,25 +10230,13 @@ dependencies = [
  "base64ct",
 ]
 
-[[package]]
-name = "peniko"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c1f594c54ccdc9bd177a726885f066bf28d20e17169e31a8a1456217b1316b4"
-dependencies = [
- "color 0.2.3",
- "kurbo",
- "peniko 0.4.0",
- "smallvec",
-]
-
 [[package]]
 name = "peniko"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9"
 dependencies = [
- "color 0.3.0",
+ "color",
  "kurbo",
  "smallvec",
 ]
@@ -10590,15 +10661,15 @@ dependencies = [
 
 [[package]]
 name = "polling"
-version = "3.7.4"
+version = "3.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
- "hermit-abi 0.4.0",
+ "hermit-abi 0.5.1",
  "pin-project-lite",
- "rustix 0.38.44",
+ "rustix 1.0.7",
  "tracing",
  "windows-sys 0.59.0",
 ]
@@ -10636,6 +10707,15 @@ dependencies = [
  "portable-atomic",
 ]
 
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec 0.11.2",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -10648,7 +10728,7 @@ version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
 dependencies = [
- "zerocopy 0.8.25",
+ "zerocopy",
 ]
 
 [[package]]
@@ -10937,9 +11017,9 @@ dependencies = [
 
 [[package]]
 name = "quinn"
-version = "0.11.7"
+version = "0.11.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
+checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
 dependencies = [
  "bytes",
  "cfg_aliases 0.2.1",
@@ -10957,12 +11037,13 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.11"
+version = "0.11.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
+checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
 dependencies = [
  "bytes",
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
+ "lru-slab",
  "rand 0.9.1",
  "ring",
  "rustc-hash 2.1.1",
@@ -11105,7 +11186,7 @@ version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
 dependencies = [
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
 ]
 
 [[package]]
@@ -11176,7 +11257,7 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cassowary",
  "compact_str",
  "crossterm 0.28.1",
@@ -11284,9 +11365,9 @@ dependencies = [
 
 [[package]]
 name = "read-fonts"
-version = "0.25.3"
+version = "0.29.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f"
+checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
 dependencies = [
  "bytemuck",
  "font-types",
@@ -11307,7 +11388,7 @@ version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -11470,7 +11551,7 @@ dependencies = [
  "http-body 1.0.1",
  "http-body-util",
  "hyper 1.6.0",
- "hyper-rustls 0.27.5",
+ "hyper-rustls 0.27.6",
  "hyper-tls",
  "hyper-util",
  "ipnet",
@@ -11555,8 +11636,8 @@ version = "1.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
 dependencies = [
- "ahash 0.8.11",
- "bitflags 2.9.0",
+ "ahash 0.8.12",
+ "bitflags 2.9.1",
  "instant",
  "num-traits",
  "once_cell",
@@ -11641,7 +11722,7 @@ version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1630639f4dbc1c71ad7b704cda2171584c80735c502efae94804d02763fc6f7d"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bzip2",
  "chrono",
  "cpio",
@@ -11743,7 +11824,7 @@ version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
@@ -11756,7 +11837,7 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "errno",
  "libc",
  "linux-raw-sys 0.9.4",
@@ -11800,7 +11881,7 @@ dependencies = [
  "once_cell",
  "ring",
  "rustls-pki-types",
- "rustls-webpki 0.103.2",
+ "rustls-webpki 0.103.3",
  "subtle",
  "zeroize",
 ]
@@ -11893,9 +11974,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-webpki"
-version = "0.103.2"
+version = "0.103.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
 dependencies = [
  "aws-lc-rs",
  "ring",
@@ -11905,22 +11986,22 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.20"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
 
 [[package]]
 name = "rustybuzz"
-version = "0.18.0"
+version = "0.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181"
+checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bytemuck",
  "core_maths",
  "log",
  "smallvec",
- "ttf-parser 0.24.1",
+ "ttf-parser",
  "unicode-bidi-mirroring",
  "unicode-ccc",
  "unicode-properties",
@@ -12079,7 +12160,7 @@ version = "2.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.9.4",
  "core-foundation-sys",
  "libc",
@@ -12092,7 +12173,7 @@ version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.10.0",
  "core-foundation-sys",
  "libc",
@@ -12131,13 +12212,13 @@ dependencies = [
 
 [[package]]
 name = "selectors"
-version = "0.27.0"
+version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b75e048a93e14929e68e37b82e207db957cbb368375a80ed3ca28ac75080856"
+checksum = "d61a96a0a2d04f964888e003ec83e3172159a16e81b35de9f6ab85d8767cf2b1"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cssparser 0.35.0",
- "derive_more 0.99.20",
+ "derive_more 2.0.1",
  "fxhash",
  "log",
  "new_debug_unreachable",
@@ -12150,6 +12231,41 @@ dependencies = [
  "to_shmem_derive",
 ]
 
+[[package]]
+name = "self-replace"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
+dependencies = [
+ "fastrand",
+ "tempfile",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "self_update"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d832c086ece0dacc29fb2947bb4219b8f6e12fe9e40b7108f9e57c4224e47b5c"
+dependencies = [
+ "either",
+ "flate2",
+ "hyper 1.6.0",
+ "indicatif",
+ "log",
+ "quick-xml 0.37.5",
+ "regex",
+ "reqwest 0.12.15",
+ "self-replace",
+ "semver",
+ "serde_json",
+ "tar",
+ "tempfile",
+ "urlencoding",
+ "zip 2.4.2",
+ "zipsign-api",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.26"
@@ -12200,7 +12316,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
 dependencies = [
- "ordered-float",
+ "ordered-float 2.10.1",
  "serde",
 ]
 
@@ -12506,9 +12622,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "signal-hook"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -12603,9 +12719,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
 
 [[package]]
 name = "skrifa"
-version = "0.26.6"
+version = "0.31.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d"
+checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607"
 dependencies = [
  "bytemuck",
  "read-fonts",
@@ -12691,7 +12807,7 @@ version = "0.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "calloop",
  "calloop-wayland-source",
  "cursor-icon",
@@ -12791,9 +12907,9 @@ dependencies = [
 
 [[package]]
 name = "sourcemap"
-version = "9.2.0"
+version = "9.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd430118acc9fdd838557649b9b43fd0a78e3834d84a283b466f8e84720d6101"
+checksum = "bdee719193ae5c919a3ee43f64c2c0dd87f9b9a451d67918a2a5ec2e3c70561c"
 dependencies = [
  "base64-simd 0.8.0",
  "bitvec",
@@ -12834,7 +12950,7 @@ version = "0.3.0+sdk-1.3.268.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -12849,9 +12965,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
 dependencies = [
  "sqlx-core",
  "sqlx-macros",
@@ -12862,9 +12978,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-core"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
 dependencies = [
  "base64 0.22.1",
  "bigdecimal",
@@ -12909,9 +13025,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-macros"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -12922,9 +13038,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-macros-core"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
 dependencies = [
  "dotenvy",
  "either",
@@ -12941,21 +13057,20 @@ dependencies = [
  "sqlx-postgres",
  "sqlx-sqlite",
  "syn 2.0.101",
- "tempfile",
  "tokio",
  "url",
 ]
 
 [[package]]
 name = "sqlx-mysql"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
 dependencies = [
  "atoi",
  "base64 0.22.1",
  "bigdecimal",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "byteorder",
  "bytes",
  "chrono",
@@ -12995,15 +13110,15 @@ dependencies = [
 
 [[package]]
 name = "sqlx-postgres"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
 dependencies = [
  "atoi",
  "base64 0.22.1",
  "bigdecimal",
  "bit-vec 0.6.3",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "byteorder",
  "chrono",
  "crc 3.3.0",
@@ -13042,9 +13157,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-sqlite"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
 dependencies = [
  "atoi",
  "chrono",
@@ -13242,28 +13357,27 @@ dependencies = [
 
 [[package]]
 name = "stylo"
-version = "0.2.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfacf8821bb157fbd6cebaa682f22bcebddc38e4165cdedba0061dae24b241c2"
+checksum = "d1cdb2ebcdd49e8e2524d3045202eb472c4261bb9956294fd52ccc1200b8e0af"
 dependencies = [
  "app_units",
  "arrayvec",
  "atomic_refcell",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "byteorder",
  "cssparser 0.35.0",
- "derive_more 0.99.20",
+ "derive_more 2.0.1",
  "encoding_rs",
  "euclid",
  "fxhash",
  "icu_segmenter",
  "indexmap 2.9.0",
- "itertools 0.10.5",
+ "itertools 0.14.0",
  "itoa 1.0.15",
  "lazy_static",
  "log",
  "malloc_size_of_derive",
- "markup5ever 0.15.0",
  "matches",
  "mime",
  "new_debug_unreachable",
@@ -13275,7 +13389,7 @@ dependencies = [
  "precomputed-hash",
  "rayon",
  "rayon-core",
- "selectors 0.27.0",
+ "selectors 0.29.0",
  "serde",
  "servo_arc 0.4.0",
  "smallbitvec",
@@ -13293,17 +13407,17 @@ dependencies = [
  "to_shmem",
  "to_shmem_derive",
  "uluru",
- "unicode-bidi",
  "url",
  "void",
  "walkdir",
+ "web_atoms",
 ]
 
 [[package]]
 name = "stylo_atoms"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b943c983729930ee70141ab6515604bb33ff5de8d7626024072c38a27c8023bb"
+checksum = "4a2e2d0a6532ae003a87e9a44d35e74175af06cbba191ba80947ba8e9ea369b3"
 dependencies = [
  "string_cache",
  "string_cache_codegen",
@@ -13311,15 +13425,15 @@ dependencies = [
 
 [[package]]
 name = "stylo_config"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2e53afe5289d75063564e60aa59c591a2c496d3425f2bb2a3002251785e2058"
+checksum = "1a308fdc37610b1b9c6dbce4244a91f7bdf88bf8a13ccfbc50c212e09b29bc7d"
 
 [[package]]
 name = "stylo_derive"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e745b58d581ba8eb932825e684634eae9be848fd49c086b3f17ccb7d6c417594"
+checksum = "8384a2ebb61abbde4466dfc9d5dcda134dfa939b77bfb2df878d1c7dffe081f1"
 dependencies = [
  "darling",
  "proc-macro2",
@@ -13330,24 +13444,24 @@ dependencies = [
 
 [[package]]
 name = "stylo_dom"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06606e83c4feb986ea601a2c7d625aa5c303ecfc9526494480f6e599b8d6956e"
+checksum = "d6bce5a8c7b6d5657952caeeec50213d356d1ca2f2b042cf4594a897701282e9"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "stylo_malloc_size_of",
 ]
 
 [[package]]
 name = "stylo_malloc_size_of"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93db6fdbcde7f037f7810873374d85a2b3d37686b046577943818167a6e52d3f"
+checksum = "fc88bd0d890c656478a8bfaab59e0e25d405149ef1117917d792540ab75b47cc"
 dependencies = [
  "app_units",
  "cssparser 0.35.0",
  "euclid",
- "selectors 0.27.0",
+ "selectors 0.29.0",
  "servo_arc 0.4.0",
  "smallbitvec",
  "smallvec",
@@ -13358,15 +13472,15 @@ dependencies = [
 
 [[package]]
 name = "stylo_static_prefs"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "decb57071c4b4d5690a9719fb04a07cf2fab0fa3df99a830ef735192a1a98e5d"
+checksum = "500f379645e8a87fd03fe88607a5edcb0d8e4e423baa74ba52db198a06a0c261"
 
 [[package]]
 name = "stylo_taffy"
-version = "0.1.0-alpha.1"
+version = "0.1.0-alpha.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6da84b2bae9d25a2156b7cf89acab90683ac4d25dcb3fe1817dc9e77a20ce2f"
+checksum = "626338f08849d841c21e9a4910dbe0cbbf10f2812942c29ac360bed1327bf3ec"
 dependencies = [
  "stylo",
  "taffy",
@@ -13374,16 +13488,16 @@ dependencies = [
 
 [[package]]
 name = "stylo_traits"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f611ebeee90c0d255bf4d4fadc05f964ef019f8aaed307108fa859c826f2753"
+checksum = "244bfd473f321ff0258f5b2986e218d5f72507fa405fd131b8dbb5ebac166663"
 dependencies = [
  "app_units",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cssparser 0.35.0",
  "euclid",
  "malloc_size_of_derive",
- "selectors 0.27.0",
+ "selectors 0.29.0",
  "serde",
  "servo_arc 0.4.0",
  "stylo_atoms",
@@ -13396,11 +13510,11 @@ dependencies = [
 
 [[package]]
 name = "subsecond"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "js-sys",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "memfd",
  "memmap2",
  "serde",
@@ -13411,9 +13525,18 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "subsecond-tls-harness"
+version = "0.1.0"
+dependencies = [
+ "cross-tls-crate",
+ "cross-tls-crate-dylib",
+ "dioxus-devtools",
+]
+
 [[package]]
 name = "subsecond-types"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "serde",
 ]
@@ -13445,7 +13568,7 @@ dependencies = [
 
 [[package]]
 name = "suspense-carousel"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "async-std",
  "dioxus",
@@ -13470,9 +13593,9 @@ dependencies = [
 
 [[package]]
 name = "swash"
-version = "0.2.2"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fae9a562c7b46107d9c78cd78b75bbe1e991c16734c0aee8ff0ee711fb8b620a"
+checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3"
 dependencies = [
  "skrifa",
  "yazi",
@@ -13540,7 +13663,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96b6a5ef4cfec51d3fa30b73600f206453a37fc30cf1141e4644a57b1ed88616"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
  "anyhow",
  "dashmap 5.5.3",
  "once_cell",
@@ -13608,7 +13731,7 @@ version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "82f448db2d1c52ffd2bd3788d89cafd8b5a75b97f0dc8aae00874dda2647f6b6"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "is-macro",
  "num-bigint",
  "phf 0.11.3",
@@ -13739,7 +13862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09fdc36d220bcd51f70b1d78bdd8c1e1a172b4e594c385bdd9614b84a7c0e112"
 dependencies = [
  "better_scoped_tls",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "indexmap 2.9.0",
  "once_cell",
  "phf 0.11.3",
@@ -14023,7 +14146,7 @@ version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.9.4",
  "system-configuration-sys 0.6.0",
 ]
@@ -14079,7 +14202,7 @@ version = "0.33.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "core-foundation 0.10.0",
  "core-graphics 0.24.0",
  "crossbeam-channel",
@@ -14108,7 +14231,7 @@ dependencies = [
  "unicode-segmentation",
  "url",
  "windows 0.61.1",
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
  "windows-version",
  "x11-dl",
 ]
@@ -14203,9 +14326,9 @@ dependencies = [
  "uuid",
  "walkdir",
  "which 7.0.3",
- "windows-registry 0.5.1",
+ "windows-registry 0.5.2",
  "windows-sys 0.59.0",
- "zip 2.6.1",
+ "zip 2.4.2",
 ]
 
 [[package]]
@@ -14279,7 +14402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
 dependencies = [
  "fastrand",
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
  "once_cell",
  "rustix 1.0.7",
  "windows-sys 0.59.0",
@@ -14489,7 +14612,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
 dependencies = [
  "displaydoc",
- "zerovec",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec 0.11.2",
 ]
 
 [[package]]
@@ -14546,9 +14678,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.45.0"
+version = "1.45.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
 dependencies = [
  "backtrace",
  "bytes",
@@ -14780,13 +14912,13 @@ dependencies = [
 
 [[package]]
 name = "tower-http"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1cfca9ae570b2a6efc764a88e914c29b3dfaa1fafe5f495812ae97ec9bc4d53"
+checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
 dependencies = [
  "async-compression",
  "base64 0.22.1",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "bytes",
  "futures-core",
  "futures-util",
@@ -14937,7 +15069,7 @@ dependencies = [
  "crossbeam-channel",
  "dirs 6.0.0",
  "libappindicator",
- "muda 0.16.1",
+ "muda",
  "objc2 0.6.1",
  "objc2-app-kit 0.3.1",
  "objc2-core-foundation",
@@ -14967,9 +15099,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
 [[package]]
 name = "trybuild"
-version = "1.0.104"
+version = "1.0.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898"
+checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2"
 dependencies = [
  "dissimilar",
  "glob",
@@ -14983,19 +15115,13 @@ dependencies = [
 
 [[package]]
 name = "ttf-parser"
-version = "0.24.1"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
 dependencies = [
  "core_maths",
 ]
 
-[[package]]
-name = "ttf-parser"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
-
 [[package]]
 name = "tungstenite"
 version = "0.21.0"
@@ -15187,9 +15313,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
 
 [[package]]
 name = "unicode-bidi-mirroring"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
+checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
 
 [[package]]
 name = "unicode-bom"
@@ -15199,9 +15325,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
 
 [[package]]
 name = "unicode-ccc"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
+checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
 
 [[package]]
 name = "unicode-id"
@@ -15373,9 +15499,9 @@ dependencies = [
 
 [[package]]
 name = "usvg"
-version = "0.44.0"
+version = "0.45.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6"
+checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
 dependencies = [
  "base64 0.22.1",
  "data-url 0.3.1",
@@ -15404,12 +15530,6 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
-[[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
 [[package]]
 name = "utf8-width"
 version = "0.1.7"
@@ -15430,13 +15550,15 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
 dependencies = [
- "getrandom 0.3.2",
+ "getrandom 0.3.3",
+ "js-sys",
  "serde",
  "sha1_smol",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -15470,60 +15592,48 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
 name = "vello"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d5b0bafa35e0c2e4132104576d6bcec4bf7cd0044f1760e92ecae0d4d9bc0e7"
+checksum = "df026e62e8b0d12d55ff5e91ae9114a20f82d9b856bedfdbf3abd5d1472dceed"
 dependencies = [
  "bytemuck",
  "futures-intrusive",
  "log",
- "peniko 0.3.2",
+ "peniko",
  "png",
  "skrifa",
  "static_assertions",
  "thiserror 2.0.12",
  "vello_encoding",
  "vello_shaders",
- "wgpu 23.0.1",
+ "wgpu 24.0.5",
 ]
 
 [[package]]
 name = "vello_encoding"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbbdec68dea2b39ece9f82ab15ec4cf2c4f8600ce6926df0638290702d95b3f7"
+checksum = "3c5702642f6ea77eedc12d119e1eebead0dba3cf91fe5c5d1f3efc12bf0cfaf1"
 dependencies = [
  "bytemuck",
  "guillotiere",
- "peniko 0.3.2",
+ "peniko",
  "skrifa",
  "smallvec",
 ]
 
 [[package]]
 name = "vello_shaders"
-version = "0.4.1"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e0179d74cf9131dfd7882323751d2544f3aefdfda9d16c39bbe2729799410d2"
+checksum = "381790a3779021edd9f88267c1b13b49546cb0fb164f329ee2f2587869ddf459"
 dependencies = [
  "bytemuck",
- "naga 23.1.0",
+ "naga 24.0.0",
  "thiserror 2.0.12",
  "vello_encoding",
 ]
 
-[[package]]
-name = "vello_svg"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a0fd265f55b496a8048a43c769bf15487f2be9f6642086b3468c64677cc8c87"
-dependencies = [
- "image",
- "thiserror 2.0.12",
- "usvg",
- "vello",
-]
-
 [[package]]
 name = "version-compare"
 version = "0.2.0"
@@ -15781,12 +15891,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-encoder"
-version = "0.228.0"
+version = "0.229.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d30290541f2d4242a162bbda76b8f2d8b1ac59eab3568ed6f2327d52c9b2c4"
+checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2"
 dependencies = [
  "leb128fmt",
- "wasmparser 0.228.0",
+ "wasmparser 0.229.0",
 ]
 
 [[package]]
@@ -15829,17 +15939,9 @@ dependencies = [
  "cxx-build",
 ]
 
-[[package]]
-name = "wasm-split"
-version = "0.1.0"
-dependencies = [
- "async-once-cell",
- "wasm-split-macro",
-]
-
 [[package]]
 name = "wasm-split-cli"
-version = "0.1.0"
+version = "0.7.0-alpha.1"
 dependencies = [
  "anyhow",
  "clap",
@@ -15864,7 +15966,6 @@ dependencies = [
  "futures",
  "getrandom 0.2.16",
  "js-sys",
- "once_cell",
  "reqwest 0.12.15",
  "wasm-bindgen",
  "wasm-bindgen-futures",
@@ -15873,7 +15974,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-split-macro"
-version = "0.1.0"
+version = "0.7.0-alpha.1"
 dependencies = [
  "base16",
  "digest",
@@ -15883,6 +15984,14 @@ dependencies = [
  "syn 2.0.101",
 ]
 
+[[package]]
+name = "wasm-splitter"
+version = "0.7.0-alpha.1"
+dependencies = [
+ "async-once-cell",
+ "wasm-split-macro",
+]
+
 [[package]]
 name = "wasm-streams"
 version = "0.4.2"
@@ -15898,7 +16007,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-used"
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 dependencies = [
  "id-arena",
  "tracing",
@@ -15911,8 +16020,8 @@ version = "0.214.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5309c1090e3e84dad0d382f42064e9933fdaedb87e468cc239f0eabea73ddcb6"
 dependencies = [
- "ahash 0.8.11",
- "bitflags 2.9.0",
+ "ahash 0.8.12",
+ "bitflags 2.9.1",
  "hashbrown 0.14.5",
  "indexmap 2.9.0",
  "semver",
@@ -15925,7 +16034,7 @@ version = "0.222.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa210fd1788e6b37a1d1930f3389c48e1d6ebd1a013d34fa4b7f9e3e3bf03146"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -15934,7 +16043,7 @@ version = "0.226.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "hashbrown 0.15.3",
  "indexmap 2.9.0",
  "semver",
@@ -15943,11 +16052,11 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.228.0"
+version = "0.229.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
+checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "indexmap 2.9.0",
  "semver",
 ]
@@ -15972,7 +16081,7 @@ version = "0.31.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "rustix 0.38.44",
  "wayland-backend",
  "wayland-scanner",
@@ -15984,7 +16093,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cursor-icon",
  "wayland-backend",
 ]
@@ -16006,7 +16115,7 @@ version = "0.32.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "wayland-backend",
  "wayland-client",
  "wayland-scanner",
@@ -16018,7 +16127,7 @@ version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -16031,7 +16140,7 @@ version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -16082,20 +16191,15 @@ dependencies = [
 ]
 
 [[package]]
-name = "webbrowser"
-version = "1.0.4"
+name = "web_atoms"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e"
+checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
 dependencies = [
- "core-foundation 0.10.0",
- "home",
- "jni",
- "log",
- "ndk-context",
- "objc2 0.6.1",
- "objc2-foundation 0.3.1",
- "url",
- "web-sys",
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+ "string_cache",
+ "string_cache_codegen",
 ]
 
 [[package]]
@@ -16204,9 +16308,9 @@ dependencies = [
 
 [[package]]
 name = "weezl"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
 
 [[package]]
 name = "wgpu"
@@ -16235,16 +16339,17 @@ dependencies = [
 
 [[package]]
 name = "wgpu"
-version = "23.0.1"
+version = "24.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a"
+checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353"
 dependencies = [
  "arrayvec",
- "cfg_aliases 0.1.1",
+ "bitflags 2.9.1",
+ "cfg_aliases 0.2.1",
  "document-features",
  "js-sys",
  "log",
- "naga 23.1.0",
+ "naga 24.0.0",
  "parking_lot",
  "profiling",
  "raw-window-handle 0.6.2",
@@ -16253,9 +16358,9 @@ dependencies = [
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
- "wgpu-core 23.0.1",
- "wgpu-hal 23.0.1",
- "wgpu-types 23.0.0",
+ "wgpu-core 24.0.5",
+ "wgpu-hal 24.0.4",
+ "wgpu-types 24.0.0",
 ]
 
 [[package]]
@@ -16266,7 +16371,7 @@ checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a"
 dependencies = [
  "arrayvec",
  "bit-vec 0.6.3",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cfg_aliases 0.1.1",
  "codespan-reporting 0.11.1",
  "indexmap 2.9.0",
@@ -16286,27 +16391,27 @@ dependencies = [
 
 [[package]]
 name = "wgpu-core"
-version = "23.0.1"
+version = "24.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a"
+checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499"
 dependencies = [
  "arrayvec",
  "bit-vec 0.8.0",
- "bitflags 2.9.0",
- "cfg_aliases 0.1.1",
+ "bitflags 2.9.1",
+ "cfg_aliases 0.2.1",
  "document-features",
  "indexmap 2.9.0",
  "log",
- "naga 23.1.0",
+ "naga 24.0.0",
  "once_cell",
  "parking_lot",
  "profiling",
  "raw-window-handle 0.6.2",
  "rustc-hash 1.1.0",
  "smallvec",
- "thiserror 1.0.69",
- "wgpu-hal 23.0.1",
- "wgpu-types 23.0.0",
+ "thiserror 2.0.12",
+ "wgpu-hal 24.0.4",
+ "wgpu-types 24.0.0",
 ]
 
 [[package]]
@@ -16319,7 +16424,7 @@ dependencies = [
  "arrayvec",
  "ash 0.37.3+1.3.251",
  "bit-set 0.5.3",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
  "cfg_aliases 0.1.1",
  "core-graphics-types 0.1.3",
@@ -16333,7 +16438,7 @@ dependencies = [
  "js-sys",
  "khronos-egl",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "log",
  "metal 0.27.0",
  "naga 0.19.2",
@@ -16356,34 +16461,35 @@ dependencies = [
 
 [[package]]
 name = "wgpu-hal"
-version = "23.0.1"
+version = "24.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821"
+checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259"
 dependencies = [
  "android_system_properties",
  "arrayvec",
  "ash 0.38.0+1.3.281",
  "bit-set 0.8.0",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block",
  "bytemuck",
- "cfg_aliases 0.1.1",
+ "cfg_aliases 0.2.1",
  "core-graphics-types 0.1.3",
- "glow 0.14.2",
+ "glow 0.16.0",
  "glutin_wgl_sys 0.6.1",
  "gpu-alloc",
  "gpu-allocator 0.27.0",
- "gpu-descriptor 0.3.1",
+ "gpu-descriptor 0.3.2",
  "js-sys",
  "khronos-egl",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "log",
- "metal 0.29.0",
- "naga 23.1.0",
+ "metal 0.31.0",
+ "naga 24.0.0",
  "ndk-sys 0.5.0+25.2.9519653",
  "objc",
  "once_cell",
+ "ordered-float 4.6.0",
  "parking_lot",
  "profiling",
  "range-alloc",
@@ -16391,33 +16497,46 @@ dependencies = [
  "renderdoc-sys",
  "rustc-hash 1.1.0",
  "smallvec",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "wasm-bindgen",
  "web-sys",
- "wgpu-types 23.0.0",
+ "wgpu-types 24.0.0",
  "windows 0.58.0",
  "windows-core 0.58.0",
 ]
 
+[[package]]
+name = "wgpu-texture"
+version = "0.0.0"
+dependencies = [
+ "bytemuck",
+ "color",
+ "dioxus",
+ "dioxus-native",
+ "tracing-subscriber",
+ "wgpu 24.0.5",
+]
+
 [[package]]
 name = "wgpu-types"
 version = "0.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "js-sys",
  "web-sys",
 ]
 
 [[package]]
 name = "wgpu-types"
-version = "23.0.0"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068"
+checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "js-sys",
+ "log",
  "web-sys",
 ]
 
@@ -16550,7 +16669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
 dependencies = [
  "windows-collections",
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
  "windows-future",
  "windows-link",
  "windows-numerics",
@@ -16562,7 +16681,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
 dependencies = [
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
 ]
 
 [[package]]
@@ -16601,25 +16720,26 @@ dependencies = [
 
 [[package]]
 name = "windows-core"
-version = "0.61.0"
+version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
 dependencies = [
  "windows-implement 0.60.0",
  "windows-interface 0.59.1",
  "windows-link",
- "windows-result 0.3.2",
- "windows-strings 0.4.0",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
 ]
 
 [[package]]
 name = "windows-future"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
 dependencies = [
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
  "windows-link",
+ "windows-threading",
 ]
 
 [[package]]
@@ -16700,7 +16820,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
 dependencies = [
- "windows-core 0.61.0",
+ "windows-core 0.61.2",
  "windows-link",
 ]
 
@@ -16710,20 +16830,20 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
 dependencies = [
- "windows-result 0.3.2",
+ "windows-result 0.3.4",
  "windows-strings 0.3.1",
  "windows-targets 0.53.0",
 ]
 
 [[package]]
 name = "windows-registry"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
+checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
 dependencies = [
  "windows-link",
- "windows-result 0.3.2",
- "windows-strings 0.4.0",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
 ]
 
 [[package]]
@@ -16746,9 +16866,9 @@ dependencies = [
 
 [[package]]
 name = "windows-result"
-version = "0.3.2"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
 dependencies = [
  "windows-link",
 ]
@@ -16774,9 +16894,9 @@ dependencies = [
 
 [[package]]
 name = "windows-strings"
-version = "0.4.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
 dependencies = [
  "windows-link",
 ]
@@ -16879,6 +16999,15 @@ dependencies = [
  "windows_x86_64_msvc 0.53.0",
 ]
 
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-version"
 version = "0.1.4"
@@ -17070,14 +17199,14 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
 
 [[package]]
 name = "winit"
-version = "0.30.10"
+version = "0.30.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0d05bd8908e14618c9609471db04007e644fd9cce6529756046cfc577f9155e"
+checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
 dependencies = [
- "ahash 0.8.11",
+ "ahash 0.8.12",
  "android-activity",
  "atomic-waker",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "block2 0.5.1",
  "bytemuck",
  "calloop",
@@ -17160,20 +17289,20 @@ version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
-name = "write16"
-version = "1.0.0"
+name = "writeable"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
 
 [[package]]
 name = "writeable"
-version = "0.5.5"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
 
 [[package]]
 name = "wry"
@@ -17183,7 +17312,7 @@ checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0"
 dependencies = [
  "base64 0.22.1",
  "block",
- "cocoa 0.26.0",
+ "cocoa",
  "core-graphics 0.24.0",
  "crossbeam-channel",
  "dpi",
@@ -17254,7 +17383,7 @@ dependencies = [
  "as-raw-xcb-connection",
  "gethostname",
  "libc",
- "libloading 0.8.6",
+ "libloading 0.8.7",
  "once_cell",
  "rustix 0.38.44",
  "x11rb-protocol",
@@ -17350,7 +17479,7 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
 dependencies = [
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "dlib",
  "log",
  "once_cell",
@@ -17416,7 +17545,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
 dependencies = [
  "serde",
  "stable_deref_trait",
- "yoke-derive",
+ "yoke-derive 0.7.5",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive 0.8.0",
  "zerofrom",
 ]
 
@@ -17432,6 +17573,18 @@ dependencies = [
  "synstructure 0.13.2",
 ]
 
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "synstructure 0.13.2",
+]
+
 [[package]]
 name = "zbus"
 version = "4.4.0"
@@ -17454,7 +17607,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "hex",
- "nix",
+ "nix 0.29.0",
  "ordered-stream",
  "rand 0.8.5",
  "serde",
@@ -17472,9 +17625,9 @@ dependencies = [
 
 [[package]]
 name = "zbus"
-version = "5.6.0"
+version = "5.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2522b82023923eecb0b366da727ec883ace092e7887b61d3da5139f26b44da58"
+checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
 dependencies = [
  "async-broadcast",
  "async-recursion",
@@ -17484,7 +17637,7 @@ dependencies = [
  "futures-core",
  "futures-lite",
  "hex",
- "nix",
+ "nix 0.30.1",
  "ordered-stream",
  "serde",
  "serde_repr",
@@ -17493,9 +17646,9 @@ dependencies = [
  "uds_windows",
  "windows-sys 0.59.0",
  "winnow 0.7.10",
- "zbus_macros 5.6.0",
+ "zbus_macros 5.7.1",
  "zbus_names 4.2.0",
- "zvariant 5.5.1",
+ "zvariant 5.5.3",
 ]
 
 [[package]]
@@ -17537,16 +17690,16 @@ dependencies = [
 
 [[package]]
 name = "zbus_macros"
-version = "5.6.0"
+version = "5.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d2e12843c75108c00c618c2e8ef9675b50b6ec095b36dc965f2e5aed463c15"
+checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
 dependencies = [
  "proc-macro-crate 3.3.0",
  "proc-macro2",
  "quote",
  "syn 2.0.101",
  "zbus_names 4.2.0",
- "zvariant 5.5.1",
+ "zvariant 5.5.3",
  "zvariant_utils 3.2.0",
 ]
 
@@ -17570,7 +17723,7 @@ dependencies = [
  "serde",
  "static_assertions",
  "winnow 0.7.10",
- "zvariant 5.5.1",
+ "zvariant 5.5.3",
 ]
 
 [[package]]
@@ -17588,18 +17741,9 @@ dependencies = [
 
 [[package]]
 name = "zeno"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc0de2315dc13d00e5df3cd6b8d2124a6eaec6a2d4b6a1c5f37b7efad17fcc17"
-
-[[package]]
-name = "zerocopy"
-version = "0.7.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
-dependencies = [
- "zerocopy-derive 0.7.35",
-]
+checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
 
 [[package]]
 name = "zerocopy"
@@ -17607,18 +17751,7 @@ version = "0.8.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
 dependencies = [
- "zerocopy-derive 0.8.25",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.101",
+ "zerocopy-derive",
 ]
 
 [[package]]
@@ -17673,15 +17806,37 @@ dependencies = [
  "syn 2.0.101",
 ]
 
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke 0.8.0",
+ "zerofrom",
+]
+
 [[package]]
 name = "zerovec"
 version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
 dependencies = [
- "yoke",
+ "yoke 0.7.5",
+ "zerofrom",
+ "zerovec-derive 0.10.3",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke 0.8.0",
  "zerofrom",
- "zerovec-derive",
+ "zerovec-derive 0.11.1",
 ]
 
 [[package]]
@@ -17695,6 +17850,17 @@ dependencies = [
  "syn 2.0.101",
 ]
 
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
 [[package]]
 name = "zip"
 version = "0.6.6"
@@ -17709,16 +17875,19 @@ dependencies = [
 
 [[package]]
 name = "zip"
-version = "2.6.1"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
+checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
 dependencies = [
  "arbitrary",
  "crc32fast",
  "crossbeam-utils",
+ "displaydoc",
  "flate2",
  "indexmap 2.9.0",
  "memchr",
+ "thiserror 2.0.12",
+ "time",
  "zopfli",
 ]
 
@@ -17733,6 +17902,17 @@ dependencies = [
  "thiserror 1.0.69",
 ]
 
+[[package]]
+name = "zipsign-api"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0"
+dependencies = [
+ "base64 0.22.1",
+ "ed25519-dalek",
+ "thiserror 2.0.12",
+]
+
 [[package]]
 name = "zopfli"
 version = "0.8.2"
@@ -17812,16 +17992,16 @@ dependencies = [
 
 [[package]]
 name = "zvariant"
-version = "5.5.1"
+version = "5.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "557e89d54880377a507c94cd5452f20e35d14325faf9d2958ebeadce0966c1b2"
+checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1"
 dependencies = [
  "endi",
  "enumflags2",
  "serde",
  "url",
  "winnow 0.7.10",
- "zvariant_derive 5.5.1",
+ "zvariant_derive 5.5.3",
  "zvariant_utils 3.2.0",
 ]
 
@@ -17840,9 +18020,9 @@ dependencies = [
 
 [[package]]
 name = "zvariant_derive"
-version = "5.5.1"
+version = "5.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "757779842a0d242061d24c28be589ce392e45350dfb9186dfd7a042a2e19870c"
+checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580"
 dependencies = [
  "proc-macro-crate 3.3.0",
  "proc-macro2",

+ 70 - 64
Cargo.toml

@@ -94,6 +94,9 @@ members = [
     # subsecond
     "packages/subsecond/subsecond",
     "packages/subsecond/subsecond-types",
+    "packages/subsecond/subsecond-tests/cross-tls-crate",
+    "packages/subsecond/subsecond-tests/cross-tls-crate-dylib",
+    "packages/subsecond/subsecond-tests/cross-tls-test",
 
     # Full project examples
     "example-projects/fullstack-hackernews",
@@ -109,10 +112,14 @@ members = [
     "examples/fullstack-streaming",
     "examples/fullstack-desktop",
     "examples/fullstack-auth",
+    "examples/fullstack-websockets",
+    "examples/wgpu-texture",
 
     # Playwright tests
     "packages/playwright-tests/liveview",
     "packages/playwright-tests/web",
+    "packages/playwright-tests/web-routing",
+    "packages/playwright-tests/web-hash-routing",
     "packages/playwright-tests/barebones-template",
     "packages/playwright-tests/fullstack",
     "packages/playwright-tests/fullstack-mounted",
@@ -125,78 +132,79 @@ members = [
 ]
 
 [workspace.package]
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 
 # dependencies that are shared across packages
 [workspace.dependencies]
-dioxus = { path = "packages/dioxus", version = "0.6.2" }
-dioxus-lib = { path = "packages/dioxus-lib", version = "0.6.2" }
-dioxus-core = { path = "packages/core", version = "0.6.2" }
-dioxus-core-types = { path = "packages/core-types", version = "0.6.2" }
-dioxus-core-macro = { path = "packages/core-macro", version = "0.6.2" }
-dioxus-config-macro = { path = "packages/config-macro", version = "0.6.2" }
-dioxus-router = { path = "packages/router", version = "0.6.2" }
-dioxus-router-macro = { path = "packages/router-macro", version = "0.6.2" }
-dioxus-document = { path = "packages/document", version = "0.6.2", default-features = false }
-dioxus-history = { path = "packages/history", version = "0.6.2", default-features = false }
-dioxus-html = { path = "packages/html", version = "0.6.2", default-features = false }
-dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.2" }
-dioxus-hooks = { path = "packages/hooks", version = "0.6.2" }
-dioxus-web = { path = "packages/web", version = "0.6.2", default-features = false }
-dioxus-isrg = { path = "packages/isrg", version = "0.6.2" }
-dioxus-ssr = { path = "packages/ssr", version = "0.6.2", default-features = false }
-dioxus-desktop = { path = "packages/desktop", version = "0.6.2", default-features = false }
-dioxus-mobile = { path = "packages/mobile", version = "0.6.2" }
-dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.2" }
-dioxus-liveview = { path = "packages/liveview", version = "0.6.2" }
-dioxus-autofmt = { path = "packages/autofmt", version = "0.6.2" }
-dioxus-check = { path = "packages/check", version = "0.6.2" }
-dioxus-rsx = { path = "packages/rsx", version = "0.6.2" }
-dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.2" }
-dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.2" }
-dioxus-signals = { path = "packages/signals", version = "0.6.2" }
-dioxus-cli-config = { path = "packages/cli-config", version = "0.6.2" }
-dioxus-cli-opt = { path = "packages/cli-opt", version = "0.6.2" }
-dioxus-devtools = { path = "packages/devtools", version = "0.6.2" }
-dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.2" }
-dioxus-server = { path = "packages/server", version = "0.6.2" }
-dioxus-fullstack = { path = "packages/fullstack", version = "0.6.2" }
-dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" }
-dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.6.3" }
-dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false }
-dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" }
-dioxus-logger = { path = "packages/logger", version = "0.6.2" }
-dioxus-native = { path = "packages/native", version = "0.6.2" }
-dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.6.2" }
-dioxus-config-macros = { path = "packages/config-macros", version = "0.6.3" }
-const-serialize = { path = "packages/const-serialize", version = "0.6.2" }
-const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.6.2" }
-generational-box = { path = "packages/generational-box", version = "0.6.2" }
-lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.2" }
+dioxus = { path = "packages/dioxus", version = "0.7.0-alpha.1" }
+dioxus-lib = { path = "packages/dioxus-lib", version = "0.7.0-alpha.1", default-features = false }
+dioxus-core = { path = "packages/core", version = "0.7.0-alpha.1" }
+dioxus-core-types = { path = "packages/core-types", version = "0.7.0-alpha.1" }
+dioxus-core-macro = { path = "packages/core-macro", version = "0.7.0-alpha.1" }
+dioxus-config-macro = { path = "packages/config-macro", version = "0.7.0-alpha.1" }
+dioxus-router = { path = "packages/router", version = "0.7.0-alpha.1" }
+dioxus-router-macro = { path = "packages/router-macro", version = "0.7.0-alpha.1" }
+dioxus-document = { path = "packages/document", version = "0.7.0-alpha.1", default-features = false }
+dioxus-history = { path = "packages/history", version = "0.7.0-alpha.1", default-features = false }
+dioxus-html = { path = "packages/html", version = "0.7.0-alpha.1", default-features = false }
+dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.7.0-alpha.1" }
+dioxus-hooks = { path = "packages/hooks", version = "0.7.0-alpha.1" }
+dioxus-web = { path = "packages/web", version = "0.7.0-alpha.1", default-features = false }
+dioxus-isrg = { path = "packages/isrg", version = "0.7.0-alpha.1" }
+dioxus-ssr = { path = "packages/ssr", version = "0.7.0-alpha.1", default-features = false }
+dioxus-desktop = { path = "packages/desktop", version = "0.7.0-alpha.1", default-features = false }
+dioxus-mobile = { path = "packages/mobile", version = "0.7.0-alpha.1" }
+dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.0-alpha.1" }
+dioxus-liveview = { path = "packages/liveview", version = "0.7.0-alpha.1" }
+dioxus-autofmt = { path = "packages/autofmt", version = "0.7.0-alpha.1" }
+dioxus-check = { path = "packages/check", version = "0.7.0-alpha.1" }
+dioxus-rsx = { path = "packages/rsx", version = "0.7.0-alpha.1" }
+dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.0-alpha.1" }
+dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.0-alpha.1" }
+dioxus-signals = { path = "packages/signals", version = "0.7.0-alpha.1" }
+dioxus-cli-config = { path = "packages/cli-config", version = "0.7.0-alpha.1" }
+dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.0-alpha.1" }
+dioxus-devtools = { path = "packages/devtools", version = "0.7.0-alpha.1" }
+dioxus-devtools-types = { path = "packages/devtools-types", version = "0.7.0-alpha.1" }
+dioxus-server = { path = "packages/server", version = "0.7.0-alpha.1" }
+dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-alpha.1" }
+dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.7.0-alpha.1" }
+dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.7.0-alpha.1" }
+dioxus_server_macro = { path = "packages/server-macro", version = "0.7.0-alpha.1", default-features = false }
+dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.7.0-alpha.1" }
+dioxus-logger = { path = "packages/logger", version = "0.7.0-alpha.1" }
+dioxus-native = { path = "packages/native", version = "0.7.0-alpha.1" }
+dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.7.0-alpha.1" }
+dioxus-config-macros = { path = "packages/config-macros", version = "0.7.0-alpha.1" }
+const-serialize = { path = "packages/const-serialize", version = "0.7.0-alpha.1" }
+const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.7.0-alpha.1" }
+generational-box = { path = "packages/generational-box", version = "0.7.0-alpha.1" }
+lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.0-alpha.1" }
 
 # subsecond
-subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.6.3" }
-subsecond = { path = "packages/subsecond/subsecond", version = "0.6.3" }
+subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.7.0-alpha.1" }
+subsecond = { path = "packages/subsecond/subsecond", version = "0.7.0-alpha.1" }
 
 # manganis
-manganis = { path = "packages/manganis/manganis", version = "0.6.2" }
-manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.2" }
-manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.2" }
+manganis = { path = "packages/manganis/manganis", version = "0.7.0-alpha.1" }
+manganis-core = { path = "packages/manganis/manganis-core", version = "0.7.0-alpha.1" }
+manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.0-alpha.1" }
 
 # wasm-split
-wasm-split = { path = "packages/wasm-split/wasm-split", version = "0.1.0" }
-wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.1.0" }
-wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.1.0" }
-wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.1.0" }
+wasm-splitter = { path = "packages/wasm-split/wasm-split", version = "0.7.0-alpha.1" }
+wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.0-alpha.1" }
+wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.7.0-alpha.1" }
+wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.7.0-alpha.1" }
+wasm-used = { path = "packages/wasm-split/wasm-used", version = "0.7.0-alpha.1" }
 
-depinfo = { path = "packages/depinfo", version = "0.6.3" }
+depinfo = { path = "packages/depinfo", version = "0.7.0-alpha.1" }
 warnings = { version = "0.2.1" }
 
 # a fork of pretty please for tests - let's get off of this if we can!
 prettier-please = { version = "0.3.0", features = ["verbatim"] }
 anyhow = "1.0.97"
 clap = { version = "4.5.31" }
-askama_escape = "0.10.3"
+askama_escape = "0.13.0"
 tracing = "0.1.41"
 tracing-futures = "0.2.5"
 tracing-subscriber = { version = "0.3.19", default-features = false }
@@ -219,8 +227,8 @@ thiserror = "2.0.12"
 prettyplease = { version = "0.2.30", features = ["verbatim"] }
 const_format = "0.2.34"
 cargo_toml = { version = "0.21.0" }
-tauri-utils = { version = "2.2.0" }
-tauri-bundler = { version = "2.2.4" }
+tauri-utils = { version = "=2.4.0" }
+tauri-bundler = { version = "=2.4.0" }
 lru = "0.13.0"
 async-trait = "0.1.87"
 axum = { version = "0.8.1", default-features = false }
@@ -254,7 +262,6 @@ reqwest = "0.12.12"
 owo-colors = "4.2.0"
 ciborium = "0.2.2"
 base64 = "0.22.1"
-once_cell = "1.20.3"
 uuid = "1.15.1"
 convert_case = "0.8.0"
 tungstenite = { version = "0.26.2" }
@@ -308,11 +315,10 @@ memfd = "0.6.4"
 # desktop
 wry = { version = "0.45.0", default-features = false }
 tao = { version = "0.33.0", features = ["rwh_05"] }
-webbrowser = "1.0.3"
 infer = "0.19.0"
 dunce = "1.0.5"
-urlencoding = "2.1.3"
-global-hotkey = "0.6.4"
+percent-encoding = "2.3.1"
+global-hotkey = "0.7.0"
 rfd = { version = "0.15.2", default-features = false }
 muda = "0.16.1"
 cocoa = "0.26"
@@ -398,7 +404,7 @@ documentation = "https://dioxuslabs.com"
 keywords = ["dom", "ui", "gui", "react", "wasm"]
 rust-version = "1.80.0"
 publish = false
-version = "0.6.3"
+version = "0.7.0-alpha.1"
 
 [dependencies]
 reqwest = { workspace = true, features = ["json"], optional = true }
@@ -407,7 +413,7 @@ base64 = { workspace = true, optional = true }
 http-range = { version = "0.1.5", optional = true }
 wgpu = { version = "0.19", optional = true }
 ouroboros = { version = "*", optional = true }
-wasm-split = { workspace = true }
+wasm-splitter = { workspace = true, package = "wasm-split" }
 
 [dev-dependencies]
 dioxus = { workspace = true, features = ["router"] }

+ 39 - 8
README.md

@@ -64,7 +64,7 @@
 </div>
 <br>
 <p align="center">
-  <a href="https://dioxuslabs.com/blog/release-060/">✨ Dioxus 0.6 is released - check it out here! ✨</a>
+  <a href="https://github.com/DioxusLabs/dioxus/releases/tag/v0.7.0-alpha.0">✨ Dioxus 0.7 is in alpha - test it out! ✨</a>
 </p>
 <br>
 
@@ -86,13 +86,14 @@ fn app() -> Element {
 
 - Cross-platform apps in three lines of code (web, desktop, mobile, server, and more)
 - [Ergonomic state management](https://dioxuslabs.com/blog/release-050) combines the best of React, Solid, and Svelte
-- Type-safe Routing and server functions to leverage Rust's powerful compile-time guarantees
+- Built-in featureful, type-safe, fullstack web framework
 - Integrated bundler for deploying to the web, macOS, Linux, and Windows
+- Subsecond Rust hot-patching and asset hot-reloading
 - And more! [Take a tour of Dioxus](https://dioxuslabs.com/learn/0.6/).
 
 ## Instant hot-reloading
 
-With one command, `dx serve` and your app is running. Edit your markup and styles and see the results in real time.
+With one command, `dx serve` and your app is running. Edit your markup, styles, and even Rust code and see changes in milliseconds.
 
 <div align="center">
   <img src="https://raw.githubusercontent.com/DioxusLabs/screenshots/refs/heads/main/blitz/hotreload-video.webp">
@@ -100,6 +101,36 @@ With one command, `dx serve` and your app is running. Edit your markup and style
   <!-- <video src="https://private-user-images.githubusercontent.com/10237910/386919031-6da371d5-3340-46da-84ff-628216851ba6.mov" width="500"></video> -->
 </div>
 
+## Productive, typesafe, fullstack web framework
+
+Directly call your backend from your frontend with our built-in type-safe RPC using [`server_fn`](http://crates.io/crates/server_fn). Supports streaming, suspense, bundle splitting, websockets, and more.
+
+```rust
+fn app() -> Element {
+  let mut fortune = use_signal(|| "Fetch a fortune!");
+  rsx! {
+    h1 { "{fortune}" }
+    button {
+      onclick: move |_| async move {
+        fortune.set(fetch_fortune().await.unwrap());
+      }
+    }
+  }
+}
+
+#[server]
+async fn fetch_fortune() -> ServerFnResult<String> {
+  "Dioxus is super productive!".to_string()
+}
+```
+
+## First-party primitive components
+
+Get started quickly with a complete set of primitives modeled after shadcn/ui and Radix-Primitives.
+
+<div align="center">
+  <img src="./notes/primitive-components.avif">
+</div>
 
 ## First-class Android and iOS support
 
@@ -244,7 +275,7 @@ Tauri is a framework for building desktop mobile apps where your frontend is wri
 
 Leptos is a library for building fullstack web-apps, similar to SolidJS and SolidStart. The two libraries share similar goals on the web, but have several key differences:
 
-- **Reactivity model**: Leptos uses signals to drive both reactivity and rendering, while Dioxus uses signals just for reactivity. For managing re-renders, Dioxus uses a highly optimized VirtualDOM to support desktop and mobile architectures. Both Dioxus and Leptos are extremely fast.
+- **Reactivity model**: Leptos uses signals to drive both reactivity and rendering, while Dioxus uses signals just for reactivity. For managing re-renders, Dioxus uses a highly optimized VirtualDOM to support desktop and mobile architectures. Both Dioxus and Leptos are extremely fast and comparable to the fastest web frameworks.
 
 - **Different scopes**: Dioxus provides renderers for web, desktop, mobile, LiveView, and more. We also maintain community libraries and a cross-platform SDK. Leptos has a tighter focus on the fullstack web with features that Dioxus doesn't have like islands, `<Form />` components, and other web-specific utilities.
 
@@ -271,13 +302,13 @@ view! {
 
 ### Dioxus vs Yew
 
-Yew is a framework for building single-page web apps and initially served as an inspiration for Dioxus. Unfortunately, the architecture of Yew didn't support the various features we wanted, and thus Dioxus was born.
+Yew is a framework for building reactive web apps that initially served as an inspiration for Dioxus. Yew is tightly integrated with the web but has limited utilities for server-side-rendering or alternative rendering engines. Dioxus was built as a redesign of Yew with a focus on cross-platform support, fantastic developer tooling, improved ergonomics, and a complete full-stack web story.
 
-- **Single-page apps**: Yew is designed exclusively for single-page web apps and is intrinsically tied to the web platform. Dioxus is fullstack and cross-platform, making it suitable for building web, desktop, mobile, and server apps.
+- **Full-stack capabilities**: Yew was initially designed for SPAs and remains deeply integrated with the web platform. Dioxus, in contrast, was built from the ground up for fullstack and cross-platform development, enabling seamless app creation across web, desktop, mobile, and server applications.
 
-- **Developer Tooling**: Dioxus provides a number of utilities like autoformatting, hot-reloading, and a bundler.
+- **Developer Tooling**: Dioxus offers a richer set of built-in developer tools, such as autoformatting, hot-reloading, and an integrated bundler, helping streamline the development experience.
 
-- **Ongoing support**: Dioxus is very actively maintained with new features and bug fixes being added on a daily basis.
+- **Ongoing support**: Dioxus is very actively maintained, with new features and bug fixes being fixed on a daily or weekly basis.
 
 ### Dioxus vs egui
 

+ 26 - 0
SECURITY.md

@@ -0,0 +1,26 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability in the Dioxus project, please report it **privately and responsibly** by emailing [security@dioxuslabs.com](mailto:security@dioxuslabs.com). **Do not report security issues publicly on GitHub or through issue trackers**. We take all security reports seriously and will respond promptly.
+
+## Coordinated Vulnerability Response
+
+When a security issue is reported, the Dioxus team prioritizes its resolution and coordinates a fix. We may work with affected users, upstream maintainers, and the original reporter to ensure a responsible and timely remediation. We use [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories) for secure communication and coordinated disclosure.
+
+If you're a downstream user or maintainer and believe you're affected, you can request to join the coordination process. Please email us at [security@dioxuslabs.com](mailto:security@dioxuslabs.com) with your:
+
+- Contact email
+- GitHub username(s)
+- Relevant project or ecosystem information
+
+Participation is granted at the discretion of the Dioxus team.
+
+## Security Advisory Disclosures
+
+We are committed to being transparent about security issues that affect Dioxus. Once a fix is in place, we announce advisories through:
+
+- [GitHub Release Notes](https://github.com/DioxusLabs/dioxus/releases).
+- The [RustSec Advisory Database](https://github.com/RustSec/advisory-db) (used by tools like `cargo-audit`).
+
+Users are encouraged to stay up to date with releases and monitor advisories relevant to their projects.

+ 5 - 0
_typos.toml

@@ -5,6 +5,11 @@ ratatui = "ratatui"
 lits = "lits"
 # https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event
 seeked = "seeked"
+# https://developer.apple.com/forums/thread/108953
+# udid = unique device identifier
+udid = "udid"
+# Part of Blitz's API
+unparented = "unparented"
 
 [files]
 extend-exclude = ["translations/*", "CHANGELOG.md", "*.js"]

+ 3 - 2
example-projects/ecommerce-site/README.md

@@ -6,13 +6,14 @@ This example app is a fullstack web application leveraging the [FakeStoreAPI](ht
 
 # Development
 
-1. Run the following commands to serve the application (see the tailwind example in the main Dioxus repo for more detailed information about setting up tailwind):
+1. Run the following commands to serve the application:
 
 ```bash
-npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch
 dx serve
 ```
 
+Note that in Dioxus 0.7, the Tailwind watcher is initialized automatically if a `tailwind.css` file is find in your app's root.
+
 # Status
 
 This is a work in progress. The following features are currently implemented:

+ 0 - 3
example-projects/ecommerce-site/input.css

@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;

+ 865 - 1195
example-projects/ecommerce-site/public/tailwind.css

@@ -1,1228 +1,898 @@
-/*
-! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
-  box-sizing: border-box;
-  /* 1 */
-  border-width: 0;
-  /* 2 */
-  border-style: solid;
-  /* 2 */
-  border-color: #e5e7eb;
-  /* 2 */
-}
-
-::before,
-::after {
-  --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-6. Use the user's configured `sans` font-variation-settings by default.
-*/
-
-html {
-  line-height: 1.5;
-  /* 1 */
-  -webkit-text-size-adjust: 100%;
-  /* 2 */
-  -moz-tab-size: 4;
-  /* 3 */
-  -o-tab-size: 4;
-     tab-size: 4;
-  /* 3 */
-  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-  /* 4 */
-  font-feature-settings: normal;
-  /* 5 */
-  font-variation-settings: normal;
-  /* 6 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
-  margin: 0;
-  /* 1 */
-  line-height: inherit;
-  /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
-  height: 0;
-  /* 1 */
-  color: inherit;
-  /* 2 */
-  border-top-width: 1px;
-  /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
-  -webkit-text-decoration: underline dotted;
-          text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  font-size: inherit;
-  font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
-  color: inherit;
-  text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
-  font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font family by default.
-2. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
-  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-  /* 1 */
-  font-size: 1em;
-  /* 2 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
-  font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
-  font-size: 75%;
-  line-height: 0;
-  position: relative;
-  vertical-align: baseline;
-}
-
-sub {
-  bottom: -0.25em;
-}
-
-sup {
-  top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
-  text-indent: 0;
-  /* 1 */
-  border-color: inherit;
-  /* 2 */
-  border-collapse: collapse;
-  /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
-  font-family: inherit;
-  /* 1 */
-  font-size: 100%;
-  /* 1 */
-  font-weight: inherit;
-  /* 1 */
-  line-height: inherit;
-  /* 1 */
-  color: inherit;
-  /* 1 */
-  margin: 0;
-  /* 2 */
-  padding: 0;
-  /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
-  text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-[type='button'],
-[type='reset'],
-[type='submit'] {
-  -webkit-appearance: button;
-  /* 1 */
-  background-color: transparent;
-  /* 2 */
-  background-image: none;
-  /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
-  outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
-  box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
-  vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
-  height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
-  -webkit-appearance: textfield;
-  /* 1 */
-  outline-offset: -2px;
-  /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
-  -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
-  -webkit-appearance: button;
-  /* 1 */
-  font: inherit;
-  /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
-  display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
-  margin: 0;
-}
-
-fieldset {
-  margin: 0;
-  padding: 0;
-}
-
-legend {
-  padding: 0;
-}
-
-ol,
-ul,
-menu {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
-  resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
-  cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
-  cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
-   This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
-  display: block;
-  /* 1 */
-  vertical-align: middle;
-  /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
-  max-width: 100%;
-  height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden] {
-  display: none;
-}
-
-*, ::before, ::after {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-gradient-from-position:  ;
-  --tw-gradient-via-position:  ;
-  --tw-gradient-to-position:  ;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-}
-
-::backdrop {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-gradient-from-position:  ;
-  --tw-gradient-via-position:  ;
-  --tw-gradient-to-position:  ;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-}
-
-.container {
-  width: 100%;
-}
-
-@media (min-width: 640px) {
-  .container {
-    max-width: 640px;
+/*! tailwindcss v4.1.0 | MIT License | https://tailwindcss.com */
+@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
+  @layer base {
+    *, ::before, ::after, ::backdrop {
+      --tw-translate-x: 0;
+      --tw-translate-y: 0;
+      --tw-translate-z: 0;
+      --tw-rotate-x: rotateX(0);
+      --tw-rotate-y: rotateY(0);
+      --tw-rotate-z: rotateZ(0);
+      --tw-skew-x: skewX(0);
+      --tw-skew-y: skewY(0);
+      --tw-border-style: solid;
+      --tw-font-weight: initial;
+      --tw-shadow: 0 0 #0000;
+      --tw-shadow-color: initial;
+      --tw-shadow-alpha: 100%;
+      --tw-inset-shadow: 0 0 #0000;
+      --tw-inset-shadow-color: initial;
+      --tw-inset-shadow-alpha: 100%;
+      --tw-ring-color: initial;
+      --tw-ring-shadow: 0 0 #0000;
+      --tw-inset-ring-color: initial;
+      --tw-inset-ring-shadow: 0 0 #0000;
+      --tw-ring-inset: initial;
+      --tw-ring-offset-width: 0px;
+      --tw-ring-offset-color: #fff;
+      --tw-ring-offset-shadow: 0 0 #0000;
+      --tw-duration: initial;
+    }
   }
 }
-
-@media (min-width: 768px) {
-  .container {
-    max-width: 768px;
+@layer theme, base, components, utilities;
+@layer theme {
+  :root, :host {
+    --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+      "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+      "Courier New", monospace;
+    --color-orange-300: oklch(83.7% 0.128 66.29);
+    --color-orange-400: oklch(75% 0.183 55.934);
+    --color-blue-300: oklch(80.9% 0.105 251.813);
+    --color-gray-50: oklch(98.5% 0.002 247.839);
+    --color-gray-100: oklch(96.7% 0.003 264.542);
+    --color-gray-200: oklch(92.8% 0.006 264.531);
+    --color-gray-400: oklch(70.7% 0.022 261.325);
+    --color-gray-500: oklch(55.1% 0.027 264.364);
+    --color-gray-600: oklch(44.6% 0.03 256.802);
+    --color-gray-700: oklch(37.3% 0.034 259.733);
+    --color-gray-800: oklch(27.8% 0.033 256.848);
+    --color-white: #fff;
+    --spacing: 0.25rem;
+    --container-sm: 24rem;
+    --container-md: 28rem;
+    --container-xl: 36rem;
+    --container-2xl: 42rem;
+    --text-xs: 0.75rem;
+    --text-xs--line-height: calc(1 / 0.75);
+    --text-2xl: 1.5rem;
+    --text-2xl--line-height: calc(2 / 1.5);
+    --text-3xl: 1.875rem;
+    --text-3xl--line-height: calc(2.25 / 1.875);
+    --text-5xl: 3rem;
+    --text-5xl--line-height: 1;
+    --text-6xl: 3.75rem;
+    --text-6xl--line-height: 1;
+    --font-weight-semibold: 600;
+    --font-weight-bold: 700;
+    --radius-md: 0.375rem;
+    --radius-lg: 0.5rem;
+    --default-transition-duration: 150ms;
+    --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+    --default-font-family: var(--font-sans);
+    --default-mono-font-family: var(--font-mono);
   }
 }
-
-@media (min-width: 1024px) {
-  .container {
-    max-width: 1024px;
+@layer base {
+  *, ::after, ::before, ::backdrop, ::file-selector-button {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    border: 0 solid;
   }
-}
-
-@media (min-width: 1280px) {
-  .container {
-    max-width: 1280px;
+  html, :host {
+    line-height: 1.5;
+    -webkit-text-size-adjust: 100%;
+    tab-size: 4;
+    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+    font-feature-settings: var(--default-font-feature-settings, normal);
+    font-variation-settings: var(--default-font-variation-settings, normal);
+    -webkit-tap-highlight-color: transparent;
+  }
+  hr {
+    height: 0;
+    color: inherit;
+    border-top-width: 1px;
+  }
+  abbr:where([title]) {
+    -webkit-text-decoration: underline dotted;
+    text-decoration: underline dotted;
+  }
+  h1, h2, h3, h4, h5, h6 {
+    font-size: inherit;
+    font-weight: inherit;
+  }
+  a {
+    color: inherit;
+    -webkit-text-decoration: inherit;
+    text-decoration: inherit;
+  }
+  b, strong {
+    font-weight: bolder;
+  }
+  code, kbd, samp, pre {
+    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
+    font-feature-settings: var(--default-mono-font-feature-settings, normal);
+    font-variation-settings: var(--default-mono-font-variation-settings, normal);
+    font-size: 1em;
+  }
+  small {
+    font-size: 80%;
+  }
+  sub, sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+  }
+  sub {
+    bottom: -0.25em;
+  }
+  sup {
+    top: -0.5em;
+  }
+  table {
+    text-indent: 0;
+    border-color: inherit;
+    border-collapse: collapse;
+  }
+  :-moz-focusring {
+    outline: auto;
+  }
+  progress {
+    vertical-align: baseline;
+  }
+  summary {
+    display: list-item;
+  }
+  ol, ul, menu {
+    list-style: none;
+  }
+  img, svg, video, canvas, audio, iframe, embed, object {
+    display: block;
+    vertical-align: middle;
+  }
+  img, video {
+    max-width: 100%;
+    height: auto;
+  }
+  button, input, select, optgroup, textarea, ::file-selector-button {
+    font: inherit;
+    font-feature-settings: inherit;
+    font-variation-settings: inherit;
+    letter-spacing: inherit;
+    color: inherit;
+    border-radius: 0;
+    background-color: transparent;
+    opacity: 1;
+  }
+  :where(select:is([multiple], [size])) optgroup {
+    font-weight: bolder;
+  }
+  :where(select:is([multiple], [size])) optgroup option {
+    padding-inline-start: 20px;
+  }
+  ::file-selector-button {
+    margin-inline-end: 4px;
+  }
+  ::placeholder {
+    opacity: 1;
+  }
+  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
+    ::placeholder {
+      color: color-mix(in oklab, currentColor 50%, transparent);
+    }
+  }
+  textarea {
+    resize: vertical;
+  }
+  ::-webkit-search-decoration {
+    -webkit-appearance: none;
+  }
+  ::-webkit-date-and-time-value {
+    min-height: 1lh;
+    text-align: inherit;
+  }
+  ::-webkit-datetime-edit {
+    display: inline-flex;
+  }
+  ::-webkit-datetime-edit-fields-wrapper {
+    padding: 0;
+  }
+  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
+    padding-block: 0;
+  }
+  :-moz-ui-invalid {
+    box-shadow: none;
+  }
+  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
+    appearance: button;
+  }
+  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
+    height: auto;
+  }
+  [hidden]:where(:not([hidden="until-found"])) {
+    display: none !important;
   }
 }
-
-@media (min-width: 1536px) {
+@layer utilities {
+  .absolute {
+    position: absolute;
+  }
+  .fixed {
+    position: fixed;
+  }
+  .relative {
+    position: relative;
+  }
+  .inset-0 {
+    inset: calc(var(--spacing) * 0);
+  }
+  .top-0 {
+    top: calc(var(--spacing) * 0);
+  }
+  .top-1\/2 {
+    top: calc(1/2 * 100%);
+  }
+  .right-0 {
+    right: calc(var(--spacing) * 0);
+  }
+  .bottom-0 {
+    bottom: calc(var(--spacing) * 0);
+  }
+  .left-0 {
+    left: calc(var(--spacing) * 0);
+  }
+  .z-50 {
+    z-index: 50;
+  }
   .container {
-    max-width: 1536px;
+    width: 100%;
+    @media (width >= 40rem) {
+      max-width: 40rem;
+    }
+    @media (width >= 48rem) {
+      max-width: 48rem;
+    }
+    @media (width >= 64rem) {
+      max-width: 64rem;
+    }
+    @media (width >= 80rem) {
+      max-width: 80rem;
+    }
+    @media (width >= 96rem) {
+      max-width: 96rem;
+    }
+  }
+  .m-0 {
+    margin: calc(var(--spacing) * 0);
+  }
+  .m-2 {
+    margin: calc(var(--spacing) * 2);
+  }
+  .-mx-4 {
+    margin-inline: calc(var(--spacing) * -4);
+  }
+  .mx-auto {
+    margin-inline: auto;
+  }
+  .mt-2 {
+    margin-top: calc(var(--spacing) * 2);
+  }
+  .mr-1 {
+    margin-right: calc(var(--spacing) * 1);
+  }
+  .mr-2 {
+    margin-right: calc(var(--spacing) * 2);
+  }
+  .mr-3 {
+    margin-right: calc(var(--spacing) * 3);
+  }
+  .mr-6 {
+    margin-right: calc(var(--spacing) * 6);
+  }
+  .mr-8 {
+    margin-right: calc(var(--spacing) * 8);
+  }
+  .mr-10 {
+    margin-right: calc(var(--spacing) * 10);
+  }
+  .mr-12 {
+    margin-right: calc(var(--spacing) * 12);
+  }
+  .mr-14 {
+    margin-right: calc(var(--spacing) * 14);
+  }
+  .mr-16 {
+    margin-right: calc(var(--spacing) * 16);
+  }
+  .mr-auto {
+    margin-right: auto;
+  }
+  .mb-4 {
+    margin-bottom: calc(var(--spacing) * 4);
+  }
+  .mb-6 {
+    margin-bottom: calc(var(--spacing) * 6);
+  }
+  .mb-8 {
+    margin-bottom: calc(var(--spacing) * 8);
+  }
+  .mb-10 {
+    margin-bottom: calc(var(--spacing) * 10);
+  }
+  .mb-12 {
+    margin-bottom: calc(var(--spacing) * 12);
+  }
+  .mb-14 {
+    margin-bottom: calc(var(--spacing) * 14);
+  }
+  .mb-16 {
+    margin-bottom: calc(var(--spacing) * 16);
+  }
+  .mb-24 {
+    margin-bottom: calc(var(--spacing) * 24);
+  }
+  .ml-8 {
+    margin-left: calc(var(--spacing) * 8);
+  }
+  .block {
+    display: block;
+  }
+  .flex {
+    display: flex;
+  }
+  .hidden {
+    display: none;
+  }
+  .inline-block {
+    display: inline-block;
+  }
+  .inline-flex {
+    display: inline-flex;
+  }
+  .h-2 {
+    height: calc(var(--spacing) * 2);
+  }
+  .h-6 {
+    height: calc(var(--spacing) * 6);
+  }
+  .h-8 {
+    height: calc(var(--spacing) * 8);
+  }
+  .h-9 {
+    height: calc(var(--spacing) * 9);
+  }
+  .h-40 {
+    height: calc(var(--spacing) * 40);
+  }
+  .h-full {
+    height: 100%;
+  }
+  .w-1\/2 {
+    width: calc(1/2 * 100%);
+  }
+  .w-1\/4 {
+    width: calc(1/4 * 100%);
+  }
+  .w-1\/6 {
+    width: calc(1/6 * 100%);
+  }
+  .w-2 {
+    width: calc(var(--spacing) * 2);
+  }
+  .w-5\/6 {
+    width: calc(5/6 * 100%);
+  }
+  .w-6 {
+    width: calc(var(--spacing) * 6);
+  }
+  .w-8 {
+    width: calc(var(--spacing) * 8);
+  }
+  .w-12 {
+    width: calc(var(--spacing) * 12);
+  }
+  .w-full {
+    width: 100%;
+  }
+  .max-w-2xl {
+    max-width: var(--container-2xl);
+  }
+  .max-w-md {
+    max-width: var(--container-md);
+  }
+  .max-w-sm {
+    max-width: var(--container-sm);
+  }
+  .max-w-xl {
+    max-width: var(--container-xl);
+  }
+  .shrink-0 {
+    flex-shrink: 0;
+  }
+  .translate-1\/2 {
+    --tw-translate-x: calc(1/2 * 100%);
+    --tw-translate-y: calc(1/2 * 100%);
+    translate: var(--tw-translate-x) var(--tw-translate-y);
+  }
+  .transform {
+    transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
+  }
+  .cursor-pointer {
+    cursor: pointer;
+  }
+  .flex-col {
+    flex-direction: column;
+  }
+  .flex-row {
+    flex-direction: row;
+  }
+  .flex-wrap {
+    flex-wrap: wrap;
+  }
+  .place-items-center {
+    place-items: center;
+  }
+  .items-center {
+    align-items: center;
+  }
+  .justify-between {
+    justify-content: space-between;
+  }
+  .self-center {
+    align-self: center;
+  }
+  .overflow-y-auto {
+    overflow-y: auto;
+  }
+  .rounded {
+    border-radius: 0.25rem;
+  }
+  .rounded-full {
+    border-radius: calc(infinity * 1px);
+  }
+  .rounded-lg {
+    border-radius: var(--radius-lg);
+  }
+  .rounded-md {
+    border-radius: var(--radius-md);
+  }
+  .border {
+    border-style: var(--tw-border-style);
+    border-width: 1px;
+  }
+  .border-0 {
+    border-style: var(--tw-border-style);
+    border-width: 0px;
+  }
+  .border-r {
+    border-right-style: var(--tw-border-style);
+    border-right-width: 1px;
+  }
+  .border-b {
+    border-bottom-style: var(--tw-border-style);
+    border-bottom-width: 1px;
+  }
+  .border-b-2 {
+    border-bottom-style: var(--tw-border-style);
+    border-bottom-width: 2px;
+  }
+  .border-l {
+    border-left-style: var(--tw-border-style);
+    border-left-width: 1px;
+  }
+  .border-gray-200 {
+    border-color: var(--color-gray-200);
+  }
+  .border-transparent {
+    border-color: transparent;
+  }
+  .bg-gray-50 {
+    background-color: var(--color-gray-50);
+  }
+  .bg-gray-100 {
+    background-color: var(--color-gray-100);
+  }
+  .bg-gray-800 {
+    background-color: var(--color-gray-800);
+  }
+  .bg-orange-300 {
+    background-color: var(--color-orange-300);
+  }
+  .bg-white {
+    background-color: var(--color-white);
+  }
+  .object-cover {
+    object-fit: cover;
+  }
+  .object-scale-down {
+    object-fit: scale-down;
+  }
+  .p-2 {
+    padding: calc(var(--spacing) * 2);
+  }
+  .p-10 {
+    padding: calc(var(--spacing) * 10);
+  }
+  .px-2 {
+    padding-inline: calc(var(--spacing) * 2);
+  }
+  .px-4 {
+    padding-inline: calc(var(--spacing) * 4);
+  }
+  .px-6 {
+    padding-inline: calc(var(--spacing) * 6);
+  }
+  .px-8 {
+    padding-inline: calc(var(--spacing) * 8);
+  }
+  .px-10 {
+    padding-inline: calc(var(--spacing) * 10);
+  }
+  .px-12 {
+    padding-inline: calc(var(--spacing) * 12);
+  }
+  .py-2 {
+    padding-block: calc(var(--spacing) * 2);
+  }
+  .py-4 {
+    padding-block: calc(var(--spacing) * 4);
+  }
+  .py-5 {
+    padding-block: calc(var(--spacing) * 5);
+  }
+  .py-6 {
+    padding-block: calc(var(--spacing) * 6);
+  }
+  .py-8 {
+    padding-block: calc(var(--spacing) * 8);
+  }
+  .py-20 {
+    padding-block: calc(var(--spacing) * 20);
+  }
+  .pr-10 {
+    padding-right: calc(var(--spacing) * 10);
+  }
+  .pb-10 {
+    padding-bottom: calc(var(--spacing) * 10);
+  }
+  .pl-4 {
+    padding-left: calc(var(--spacing) * 4);
+  }
+  .pl-6 {
+    padding-left: calc(var(--spacing) * 6);
+  }
+  .text-center {
+    text-align: center;
+  }
+  .text-left {
+    text-align: left;
+  }
+  .text-2xl {
+    font-size: var(--text-2xl);
+    line-height: var(--tw-leading, var(--text-2xl--line-height));
+  }
+  .text-3xl {
+    font-size: var(--text-3xl);
+    line-height: var(--tw-leading, var(--text-3xl--line-height));
+  }
+  .text-5xl {
+    font-size: var(--text-5xl);
+    line-height: var(--tw-leading, var(--text-5xl--line-height));
+  }
+  .text-xs {
+    font-size: var(--text-xs);
+    line-height: var(--tw-leading, var(--text-xs--line-height));
+  }
+  .font-bold {
+    --tw-font-weight: var(--font-weight-bold);
+    font-weight: var(--font-weight-bold);
+  }
+  .font-semibold {
+    --tw-font-weight: var(--font-weight-semibold);
+    font-weight: var(--font-weight-semibold);
+  }
+  .text-ellipsis {
+    text-overflow: ellipsis;
+  }
+  .text-blue-300 {
+    color: var(--color-blue-300);
+  }
+  .text-gray-400 {
+    color: var(--color-gray-400);
+  }
+  .text-gray-500 {
+    color: var(--color-gray-500);
+  }
+  .text-gray-600 {
+    color: var(--color-gray-600);
+  }
+  .text-white {
+    color: var(--color-white);
+  }
+  .uppercase {
+    text-transform: uppercase;
+  }
+  .placeholder-gray-400 {
+    &::placeholder {
+      color: var(--color-gray-400);
+    }
+  }
+  .opacity-25 {
+    opacity: 25%;
+  }
+  .shadow-2xl {
+    --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
+    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+  }
+  .shadow-lg {
+    --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+  }
+  .ring-1 {
+    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
+    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+  }
+  .transition {
+    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
+    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+    transition-duration: var(--tw-duration, var(--default-transition-duration));
+  }
+  .transition-all {
+    transition-property: all;
+    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+    transition-duration: var(--tw-duration, var(--default-transition-duration));
+  }
+  .duration-200 {
+    --tw-duration: 200ms;
+    transition-duration: 200ms;
+  }
+  .hover\:bg-orange-400 {
+    &:hover {
+      @media (hover: hover) {
+        background-color: var(--color-orange-400);
+      }
+    }
+  }
+  .hover\:text-gray-600 {
+    &:hover {
+      @media (hover: hover) {
+        color: var(--color-gray-600);
+      }
+    }
+  }
+  .hover\:text-gray-700 {
+    &:hover {
+      @media (hover: hover) {
+        color: var(--color-gray-700);
+      }
+    }
+  }
+  .hover\:shadow-2xl {
+    &:hover {
+      @media (hover: hover) {
+        --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
+        box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+      }
+    }
+  }
+  .hover\:ring-4 {
+    &:hover {
+      @media (hover: hover) {
+        --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
+        box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+      }
+    }
+  }
+  .focus\:border-blue-300 {
+    &:focus {
+      border-color: var(--color-blue-300);
+    }
+  }
+  .focus\:ring-blue-300 {
+    &:focus {
+      --tw-ring-color: var(--color-blue-300);
+    }
+  }
+  .focus\:ring-transparent {
+    &:focus {
+      --tw-ring-color: transparent;
+    }
+  }
+  .focus\:outline-hidden {
+    &:focus {
+      --tw-outline-style: none;
+      outline-style: none;
+      @media (forced-colors: active) {
+        outline: 2px solid transparent;
+        outline-offset: 2px;
+      }
+    }
   }
-}
-
-.fixed {
-  position: fixed;
-}
-
-.absolute {
-  position: absolute;
-}
-
-.relative {
-  position: relative;
-}
-
-.inset-0 {
-  inset: 0px;
-}
-
-.bottom-0 {
-  bottom: 0px;
-}
-
-.left-0 {
-  left: 0px;
-}
-
-.right-0 {
-  right: 0px;
-}
-
-.top-0 {
-  top: 0px;
-}
-
-.top-1\/2 {
-  top: 50%;
-}
-
-.z-50 {
-  z-index: 50;
-}
-
-.m-0 {
-  margin: 0px;
-}
-
-.m-2 {
-  margin: 0.5rem;
-}
-
-.-mx-4 {
-  margin-left: -1rem;
-  margin-right: -1rem;
-}
-
-.mx-auto {
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.mb-10 {
-  margin-bottom: 2.5rem;
-}
-
-.mb-12 {
-  margin-bottom: 3rem;
-}
-
-.mb-14 {
-  margin-bottom: 3.5rem;
-}
-
-.mb-16 {
-  margin-bottom: 4rem;
-}
-
-.mb-24 {
-  margin-bottom: 6rem;
-}
-
-.mb-4 {
-  margin-bottom: 1rem;
-}
-
-.mb-6 {
-  margin-bottom: 1.5rem;
-}
-
-.mb-8 {
-  margin-bottom: 2rem;
-}
-
-.ml-8 {
-  margin-left: 2rem;
-}
-
-.mr-1 {
-  margin-right: 0.25rem;
-}
-
-.mr-10 {
-  margin-right: 2.5rem;
-}
-
-.mr-12 {
-  margin-right: 3rem;
-}
-
-.mr-14 {
-  margin-right: 3.5rem;
-}
-
-.mr-16 {
-  margin-right: 4rem;
-}
-
-.mr-2 {
-  margin-right: 0.5rem;
-}
-
-.mr-3 {
-  margin-right: 0.75rem;
-}
-
-.mr-6 {
-  margin-right: 1.5rem;
-}
-
-.mr-8 {
-  margin-right: 2rem;
-}
-
-.mr-auto {
-  margin-right: auto;
-}
-
-.mt-2 {
-  margin-top: 0.5rem;
-}
-
-.block {
-  display: block;
-}
-
-.inline-block {
-  display: inline-block;
-}
-
-.flex {
-  display: flex;
-}
-
-.inline-flex {
-  display: inline-flex;
-}
-
-.hidden {
-  display: none;
-}
-
-.h-2 {
-  height: 0.5rem;
-}
-
-.h-40 {
-  height: 10rem;
-}
-
-.h-6 {
-  height: 1.5rem;
-}
-
-.h-8 {
-  height: 2rem;
-}
-
-.h-9 {
-  height: 2.25rem;
-}
-
-.h-full {
-  height: 100%;
-}
-
-.w-1\/2 {
-  width: 50%;
-}
-
-.w-1\/4 {
-  width: 25%;
-}
-
-.w-1\/6 {
-  width: 16.666667%;
-}
-
-.w-12 {
-  width: 3rem;
-}
-
-.w-2 {
-  width: 0.5rem;
-}
-
-.w-5\/6 {
-  width: 83.333333%;
-}
-
-.w-6 {
-  width: 1.5rem;
-}
-
-.w-8 {
-  width: 2rem;
-}
-
-.w-full {
-  width: 100%;
-}
-
-.max-w-2xl {
-  max-width: 42rem;
-}
-
-.max-w-md {
-  max-width: 28rem;
-}
-
-.max-w-sm {
-  max-width: 24rem;
-}
-
-.max-w-xl {
-  max-width: 36rem;
-}
-
-.flex-shrink-0 {
-  flex-shrink: 0;
-}
-
-.transform {
-  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.cursor-pointer {
-  cursor: pointer;
-}
-
-.flex-row {
-  flex-direction: row;
-}
-
-.flex-col {
-  flex-direction: column;
-}
-
-.flex-wrap {
-  flex-wrap: wrap;
-}
-
-.place-items-center {
-  place-items: center;
-}
-
-.items-center {
-  align-items: center;
-}
-
-.justify-between {
-  justify-content: space-between;
-}
-
-.self-center {
-  align-self: center;
-}
-
-.overflow-y-auto {
-  overflow-y: auto;
-}
-
-.text-ellipsis {
-  text-overflow: ellipsis;
-}
-
-.rounded {
-  border-radius: 0.25rem;
-}
-
-.rounded-full {
-  border-radius: 9999px;
-}
-
-.rounded-lg {
-  border-radius: 0.5rem;
-}
-
-.rounded-md {
-  border-radius: 0.375rem;
-}
-
-.border {
-  border-width: 1px;
-}
-
-.border-0 {
-  border-width: 0px;
-}
-
-.border-b {
-  border-bottom-width: 1px;
-}
-
-.border-b-2 {
-  border-bottom-width: 2px;
-}
-
-.border-l {
-  border-left-width: 1px;
-}
-
-.border-r {
-  border-right-width: 1px;
-}
-
-.border-gray-200 {
-  --tw-border-opacity: 1;
-  border-color: rgb(229 231 235 / var(--tw-border-opacity));
-}
-
-.border-transparent {
-  border-color: transparent;
-}
-
-.bg-gray-100 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(243 244 246 / var(--tw-bg-opacity));
-}
-
-.bg-gray-50 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(249 250 251 / var(--tw-bg-opacity));
-}
-
-.bg-gray-800 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(31 41 55 / var(--tw-bg-opacity));
-}
-
-.bg-orange-300 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(253 186 116 / var(--tw-bg-opacity));
-}
-
-.bg-white {
-  --tw-bg-opacity: 1;
-  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
-}
-
-.object-cover {
-  -o-object-fit: cover;
-     object-fit: cover;
-}
-
-.object-scale-down {
-  -o-object-fit: scale-down;
-     object-fit: scale-down;
-}
-
-.p-10 {
-  padding: 2.5rem;
-}
-
-.p-2 {
-  padding: 0.5rem;
-}
-
-.px-10 {
-  padding-left: 2.5rem;
-  padding-right: 2.5rem;
-}
-
-.px-12 {
-  padding-left: 3rem;
-  padding-right: 3rem;
-}
-
-.px-2 {
-  padding-left: 0.5rem;
-  padding-right: 0.5rem;
-}
-
-.px-4 {
-  padding-left: 1rem;
-  padding-right: 1rem;
-}
-
-.px-6 {
-  padding-left: 1.5rem;
-  padding-right: 1.5rem;
-}
-
-.px-8 {
-  padding-left: 2rem;
-  padding-right: 2rem;
-}
-
-.py-2 {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-.py-20 {
-  padding-top: 5rem;
-  padding-bottom: 5rem;
-}
-
-.py-4 {
-  padding-top: 1rem;
-  padding-bottom: 1rem;
-}
-
-.py-5 {
-  padding-top: 1.25rem;
-  padding-bottom: 1.25rem;
-}
-
-.py-6 {
-  padding-top: 1.5rem;
-  padding-bottom: 1.5rem;
-}
-
-.py-8 {
-  padding-top: 2rem;
-  padding-bottom: 2rem;
-}
-
-.pb-10 {
-  padding-bottom: 2.5rem;
-}
-
-.pl-4 {
-  padding-left: 1rem;
-}
-
-.pl-6 {
-  padding-left: 1.5rem;
-}
-
-.pr-10 {
-  padding-right: 2.5rem;
-}
-
-.text-left {
-  text-align: left;
-}
-
-.text-center {
-  text-align: center;
-}
-
-.text-2xl {
-  font-size: 1.5rem;
-  line-height: 2rem;
-}
-
-.text-3xl {
-  font-size: 1.875rem;
-  line-height: 2.25rem;
-}
-
-.text-5xl {
-  font-size: 3rem;
-  line-height: 1;
-}
-
-.text-xs {
-  font-size: 0.75rem;
-  line-height: 1rem;
-}
-
-.font-bold {
-  font-weight: 700;
-}
-
-.font-semibold {
-  font-weight: 600;
-}
-
-.uppercase {
-  text-transform: uppercase;
-}
-
-.text-blue-300 {
-  --tw-text-opacity: 1;
-  color: rgb(147 197 253 / var(--tw-text-opacity));
-}
-
-.text-gray-400 {
-  --tw-text-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-text-opacity));
-}
-
-.text-gray-500 {
-  --tw-text-opacity: 1;
-  color: rgb(107 114 128 / var(--tw-text-opacity));
-}
-
-.text-gray-600 {
-  --tw-text-opacity: 1;
-  color: rgb(75 85 99 / var(--tw-text-opacity));
-}
-
-.text-white {
-  --tw-text-opacity: 1;
-  color: rgb(255 255 255 / var(--tw-text-opacity));
-}
-
-.placeholder-gray-400::-moz-placeholder {
-  --tw-placeholder-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-placeholder-opacity));
-}
-
-.placeholder-gray-400::placeholder {
-  --tw-placeholder-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-placeholder-opacity));
-}
-
-.opacity-25 {
-  opacity: 0.25;
-}
-
-.shadow-2xl {
-  --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
-  --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.shadow-lg {
-  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
-  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.ring-1 {
-  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
-  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
-  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.transition {
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
-  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.transition-all {
-  transition-property: all;
-  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-  transition-duration: 150ms;
-}
-
-.duration-200 {
-  transition-duration: 200ms;
-}
-
-.hover\:bg-orange-400:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(251 146 60 / var(--tw-bg-opacity));
-}
-
-.hover\:text-gray-600:hover {
-  --tw-text-opacity: 1;
-  color: rgb(75 85 99 / var(--tw-text-opacity));
-}
-
-.hover\:text-gray-700:hover {
-  --tw-text-opacity: 1;
-  color: rgb(55 65 81 / var(--tw-text-opacity));
-}
-
-.hover\:shadow-2xl:hover {
-  --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
-  --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-.hover\:ring-4:hover {
-  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
-  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
-  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
-}
-
-.focus\:border-blue-300:focus {
-  --tw-border-opacity: 1;
-  border-color: rgb(147 197 253 / var(--tw-border-opacity));
-}
-
-.focus\:outline-none:focus {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-}
-
-.focus\:ring-blue-300:focus {
-  --tw-ring-opacity: 1;
-  --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
-}
-
-.focus\:ring-transparent:focus {
-  --tw-ring-color: transparent;
-}
-
-@media (min-width: 768px) {
   .md\:mb-0 {
-    margin-bottom: 0px;
+    @media (width >= 48rem) {
+      margin-bottom: calc(var(--spacing) * 0);
+    }
   }
-
   .md\:w-1\/2 {
-    width: 50%;
+    @media (width >= 48rem) {
+      width: calc(1/2 * 100%);
+    }
   }
-
   .md\:w-auto {
-    width: auto;
+    @media (width >= 48rem) {
+      width: auto;
+    }
   }
-
   .md\:text-right {
-    text-align: right;
+    @media (width >= 48rem) {
+      text-align: right;
+    }
   }
-
   .md\:text-6xl {
-    font-size: 3.75rem;
-    line-height: 1;
+    @media (width >= 48rem) {
+      font-size: var(--text-6xl);
+      line-height: var(--tw-leading, var(--text-6xl--line-height));
+    }
   }
-}
-
-@media (min-width: 1024px) {
   .lg\:pl-20 {
-    padding-left: 5rem;
+    @media (width >= 64rem) {
+      padding-left: calc(var(--spacing) * 20);
+    }
   }
-}
-
-@media (min-width: 1280px) {
   .xl\:mx-auto {
-    margin-left: auto;
-    margin-right: auto;
+    @media (width >= 80rem) {
+      margin-inline: auto;
+    }
   }
-
   .xl\:mb-0 {
-    margin-bottom: 0px;
+    @media (width >= 80rem) {
+      margin-bottom: calc(var(--spacing) * 0);
+    }
   }
-
   .xl\:block {
-    display: block;
-  }
-
-  .xl\:inline-block {
-    display: inline-block;
+    @media (width >= 80rem) {
+      display: block;
+    }
   }
-
   .xl\:flex {
-    display: flex;
+    @media (width >= 80rem) {
+      display: flex;
+    }
   }
-
   .xl\:hidden {
-    display: none;
+    @media (width >= 80rem) {
+      display: none;
+    }
+  }
+  .xl\:inline-block {
+    @media (width >= 80rem) {
+      display: inline-block;
+    }
   }
-
   .xl\:w-2\/3 {
-    width: 66.666667%;
+    @media (width >= 80rem) {
+      width: calc(2/3 * 100%);
+    }
   }
-}
+}
+@property --tw-translate-x {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0;
+}
+@property --tw-translate-y {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0;
+}
+@property --tw-translate-z {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0;
+}
+@property --tw-rotate-x {
+  syntax: "*";
+  inherits: false;
+  initial-value: rotateX(0);
+}
+@property --tw-rotate-y {
+  syntax: "*";
+  inherits: false;
+  initial-value: rotateY(0);
+}
+@property --tw-rotate-z {
+  syntax: "*";
+  inherits: false;
+  initial-value: rotateZ(0);
+}
+@property --tw-skew-x {
+  syntax: "*";
+  inherits: false;
+  initial-value: skewX(0);
+}
+@property --tw-skew-y {
+  syntax: "*";
+  inherits: false;
+  initial-value: skewY(0);
+}
+@property --tw-border-style {
+  syntax: "*";
+  inherits: false;
+  initial-value: solid;
+}
+@property --tw-font-weight {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-shadow {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0 0 #0000;
+}
+@property --tw-shadow-color {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-shadow-alpha {
+  syntax: "<percentage>";
+  inherits: false;
+  initial-value: 100%;
+}
+@property --tw-inset-shadow {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0 0 #0000;
+}
+@property --tw-inset-shadow-color {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-inset-shadow-alpha {
+  syntax: "<percentage>";
+  inherits: false;
+  initial-value: 100%;
+}
+@property --tw-ring-color {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-ring-shadow {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0 0 #0000;
+}
+@property --tw-inset-ring-color {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-inset-ring-shadow {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0 0 #0000;
+}
+@property --tw-ring-inset {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-ring-offset-width {
+  syntax: "<length>";
+  inherits: false;
+  initial-value: 0px;
+}
+@property --tw-ring-offset-color {
+  syntax: "*";
+  inherits: false;
+  initial-value: #fff;
+}
+@property --tw-ring-offset-shadow {
+  syntax: "*";
+  inherits: false;
+  initial-value: 0 0 #0000;
+}
+@property --tw-duration {
+  syntax: "*";
+  inherits: false;
+}

+ 2 - 2
example-projects/ecommerce-site/src/components/nav.rs

@@ -35,7 +35,7 @@ pub fn nav() -> Element {
                             }
                         }
                     }
-                    a { class: "flex-shrink-0 xl:mx-auto text-3xl font-bold font-heading",
+                    a { class: "shrink-0 xl:mx-auto text-3xl font-bold font-heading",
                         href: "/",
                         img { class: "h-9",
                             width: "auto",
@@ -121,7 +121,7 @@ pub fn nav() -> Element {
                             }
                         }
                     }
-                    input { class: "block mb-10 py-5 px-8 bg-gray-100 rounded-md border-transparent focus:ring-blue-300 focus:border-blue-300 focus:outline-none",
+                    input { class: "block mb-10 py-5 px-8 bg-gray-100 rounded-md border-transparent focus:ring-blue-300 focus:border-blue-300 focus:outline-hidden",
                         r#type: "search",
                         placeholder: "Search",
                     }

+ 1 - 1
example-projects/ecommerce-site/src/components/product_page.rs

@@ -101,7 +101,7 @@ pub fn product_page(product_id: ReadOnlySignal<usize>) -> Element {
                                             onclick: move |_| quantity += 1,
                                             icons::icon_2 {}
                                         }
-                                        input { class: "w-12 m-0 px-2 py-4 text-center md:text-right border-0 focus:ring-transparent focus:outline-none rounded-md",
+                                        input { class: "w-12 m-0 px-2 py-4 text-center md:text-right border-0 focus:ring-transparent focus:outline-hidden rounded-md",
                                             placeholder: "1",
                                             r#type: "number",
                                             value: "{quantity}",

+ 0 - 13
example-projects/ecommerce-site/tailwind.config.js

@@ -1,13 +0,0 @@
-module.exports = {
-  mode: "all",
-  content: [
-    // include all rust, html and css files in the src directory
-    "./src/**/*.{rs,html,css}",
-    // include all html files in the output (dist) directory
-    "./dist/**/*.html",
-  ],
-  theme: {
-    extend: {},
-  },
-  plugins: [],
-};

+ 2 - 0
example-projects/ecommerce-site/tailwind.css

@@ -0,0 +1,2 @@
+@import "tailwindcss";
+@source "./src/**/*.{rs,html,css}";

+ 6 - 6
examples/README.md

@@ -44,24 +44,22 @@ cargo run --example hello_world
 
 [form](./form.rs) - Handle form submission
 
-[inputs](./inputs.rs) - Input values
-
 [nested_listeners](./nested_listeners.rs) - Nested handlers and bubbling
 
-[textarea](textarea.rs) - Text area input
-
 ### State Management
 
 [context_api](./context_api.rs) - Cross-component state sharing via Context API
 
+[counters](./counters.rs) - Mapping a `Signal<Vec<T>>` into UI elements
+
+[futures](./future.rs) - Handle async Rust with use_future, use_effect, and async event handlers
+
 ### Async
 
 [login_form](./login_form.rs) - Login endpoint example
 
 [suspense](./suspense.rs) - Render placeholders while data is loading
 
-[tasks](./tasks.rs) - Continuously run future
-
 ### SVG
 
 [svg](./svg.rs)
@@ -92,6 +90,8 @@ cargo run --example hello_world
 
 [window_zoom](./window_zoom.rs) – Zoom in or out
 
+[popup](./popup.rs) - Create a popup window and send data back to the main window
+
 ## Example Apps
 
 [calculator](./calculator.rs) - Simple calculator

+ 0 - 379
examples/assets/todomvc-native.css

@@ -1,379 +0,0 @@
-html,
-body, pre {
-    margin: 0;
-    padding: 0;
-}
-
-button {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    background: none;
-    font-size: 100%;
-    vertical-align: baseline;
-    font-family: inherit;
-    font-weight: inherit;
-    color: inherit;
-    -webkit-appearance: none;
-    appearance: none;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
-}
-
-body {
-    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
-    line-height: 1.4em;
-    background: #f5f5f5;
-    color: #4d4d4d;
-    min-width: 230px;
-    max-width: 550px;
-    margin: 0 auto;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
-    font-weight: 300;
-}
-
-:focus {
-    outline: 0;
-}
-
-.hidden {
-    display: none;
-}
-
-.todoapp {
-    background: #fff;
-    margin: 130px 0 40px 0;
-    position: relative;
-    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
-}
-
-.todoapp input::-webkit-input-placeholder {
-    font-style: italic;
-    font-weight: 300;
-    color: #e6e6e6;
-}
-
-.todoapp input::-moz-placeholder {
-    font-style: italic;
-    font-weight: 300;
-    color: #e6e6e6;
-}
-
-.todoapp input::input-placeholder {
-    font-style: italic;
-    font-weight: 300;
-    color: #e6e6e6;
-}
-
-.todoapp h1 {
-    position: absolute;
-    top: -155px;
-    width: 100%;
-    font-size: 100px;
-    font-weight: 100;
-    text-align: center;
-    color: rgba(175, 47, 47, 1.0);
-    -webkit-text-rendering: optimizeLegibility;
-    -moz-text-rendering: optimizeLegibility;
-    text-rendering: optimizeLegibility;
-}
-
-.new-todo,
-.edit {
-    position: relative;
-    margin: 0;
-    width: 100%;
-    font-size: 24px;
-    font-family: inherit;
-    font-weight: inherit;
-    line-height: 1.4em;
-    border: 0;
-    color: inherit;
-    padding: 6px;
-    border: 1px solid #999;
-    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-    box-sizing: border-box;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
-}
-
-.new-todo {
-    padding: 16px 16px 16px 60px;
-    border: none;
-    background: rgba(0, 0, 0, 0.003);
-    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
-}
-
-.main {
-    position: relative;
-    z-index: 2;
-    border-top: 1px solid #e6e6e6;
-}
-
-.toggle-all {
-    text-align: center;
-    border: none;
-    /* Mobile Safari */
-    opacity: 0;
-    position: absolute;
-}
-
-.toggle-all+label {
-    width: 60px;
-    height: 34px;
-    font-size: 0;
-    position: absolute;
-    top: -52px;
-    left: -13px;
-    -webkit-transform: rotate(90deg);
-    transform: rotate(90deg);
-}
-
-.toggle-all+label:before {
-    content: '❯';
-    font-size: 22px;
-    color: #e6e6e6;
-    padding: 10px 27px 10px 27px;
-}
-
-.toggle-all:checked+label:before {
-    color: #737373;
-}
-
-.todo-list {
-    margin: 0;
-    padding: 0;
-    list-style: none;
-}
-
-.todo-list li {
-    position: relative;
-    font-size: 24px;
-    border-bottom: 1px solid #ededed;
-}
-
-.todo-list li:last-child {
-    border-bottom: none;
-}
-
-.todo-list li.editing {
-    border-bottom: none;
-    padding: 0;
-}
-
-.todo-list li.editing .edit {
-    display: block;
-    width: 506px;
-    padding: 12px 16px;
-    margin: 0 0 0 43px;
-}
-
-.todo-list li.editing .view {
-    display: none;
-}
-
-.todo-list li .toggle {
-    text-align: center;
-    width: 40px;
-    /* auto, since non-WebKit browsers doesn't support input styling */
-    height: auto;
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    margin: auto 0;
-    border: none;
-    /* Mobile Safari */
-    -webkit-appearance: none;
-    appearance: none;
-}
-
-.todo-list li .toggle {
-    opacity: 0;
-}
-
-.todo-list li .toggle+label {
-    /*
-		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
-		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
-	*/
-    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
-    background-repeat: no-repeat;
-    background-position: center left;
-}
-
-.todo-list li .toggle:checked+label {
-    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
-}
-
-.todo-list li label {
-    word-break: break-all;
-    padding: 15px 15px 15px 60px;
-    display: block;
-    line-height: 1.2;
-    transition: color 0.4s;
-}
-
-.todo-list li.completed label {
-    color: #d9d9d9;
-    text-decoration: line-through;
-}
-
-.todo-list li .destroy {
-    display: none;
-    position: absolute;
-    top: 0;
-    right: 10px;
-    bottom: 0;
-    width: 40px;
-    height: 40px;
-    margin: auto 0;
-    font-size: 30px;
-    color: #cc9a9a;
-    margin-bottom: 11px;
-    transition: color 0.2s ease-out;
-}
-
-.todo-list li .destroy:hover {
-    color: #af5b5e;
-}
-
-.todo-list li .destroy:after {
-    content: '×';
-}
-
-.todo-list li:hover .destroy {
-    display: block;
-}
-
-.todo-list li .edit {
-    display: none;
-}
-
-.todo-list li.editing:last-child {
-    margin-bottom: -1px;
-}
-
-.footer {
-    color: #777;
-    padding: 10px 15px;
-    height: 20px;
-    text-align: center;
-    border-top: 1px solid #e6e6e6;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-}
-
-.footer:before {
-    content: '';
-    position: absolute;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    height: 50px;
-    overflow: hidden;
-    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
-}
-
-.todo-count {
-    float: left;
-    text-align: left;
-    white-space-collapse: preserve;
-}
-
-.todo-count strong {
-    font-weight: 300;
-}
-
-.filters {
-    margin: 0;
-    padding: 0;
-    list-style: none;
-    position: absolute;
-    right: 0;
-    left: 0;
-}
-
-.filters li {
-    display: inline-block;
-}
-
-.filters li a {
-    display: inline-block;
-    color: inherit;
-    margin: 3px;
-    padding: 3px 7px;
-    text-decoration: none;
-    border: 1px solid transparent;
-    border-radius: 3px;
-}
-
-.filters li a:hover {
-    border-color: rgba(175, 47, 47, 0.1);
-}
-
-.filters li a.selected {
-    border-color: rgba(175, 47, 47, 0.2);
-}
-
-.clear-completed,
-html .clear-completed:active {
-    float: right;
-    position: relative;
-    line-height: 20px;
-    text-decoration: none;
-    cursor: pointer;
-    display: inline-block;
-}
-
-.clear-completed:hover {
-    text-decoration: underline;
-}
-
-.info {
-    margin: 65px auto 0;
-    color: #bfbfbf;
-    font-size: 10px;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
-    text-align: center;
-}
-
-.info p {
-    line-height: 1;
-}
-
-.info a {
-    color: inherit;
-    text-decoration: none;
-    font-weight: 400;
-}
-
-.info a:hover {
-    text-decoration: underline;
-}
-
-
-/*
-	Hack to remove background from Mobile Safari.
-	Can't use it globally since it destroys checkboxes in Firefox
-*/
-
-@media screen and (-webkit-min-device-pixel-ratio:0) {
-    .toggle-all,
-    .todo-list li .toggle {
-        background: none;
-    }
-    .todo-list li .toggle {
-        height: 40px;
-    }
-}
-
-@media (max-width: 430px) {
-    .footer {
-        height: 50px;
-    }
-    .filters {
-        bottom: 10px;
-    }
-}

+ 5 - 0
examples/assets/todomvc.css

@@ -255,6 +255,9 @@ body {
 }
 
 .footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
     color: #777;
     padding: 10px 15px;
     height: 20px;
@@ -276,6 +279,7 @@ body {
 .todo-count {
     float: left;
     text-align: left;
+    white-space-collapse: preserve;
 }
 
 .todo-count strong {
@@ -296,6 +300,7 @@ body {
 }
 
 .filters li a {
+    display: inline-block;
     color: inherit;
     margin: 3px;
     padding: 3px 7px;

+ 6 - 0
examples/form.rs

@@ -132,5 +132,11 @@ fn app() -> Element {
                 pre { "{values:#?}" }
             }
         }
+        button {
+            onclick: move |_| {
+                println!("Values: {:#?}", values.read());
+            },
+            "Log values"
+        }
     }
 }

+ 1 - 1
examples/fullstack-auth/Cargo.toml

@@ -48,7 +48,7 @@ optional = true
 [features]
 default = []
 server = [
-    "dioxus-fullstack/server",
+    "dioxus/server",
     "dep:dioxus-cli-config",
     "dep:axum",
     "dep:tokio",

+ 1 - 1
examples/fullstack-auth/src/auth.rs

@@ -196,7 +196,7 @@ pub async fn connect_to_database() -> SqlitePool {
 pub type Session =
     axum_session_auth::AuthSession<crate::auth::User, i64, SessionSqlitePool, sqlx::SqlitePool>;
 
-pub async fn get_session() -> Result<Session, ServerFnError> {
+pub async fn get_session() -> ServerFnResult<Session> {
     extract::<Session, _>()
         .await
         .map_err(|_| ServerFnError::new("AuthSessionLayer was not found"))

+ 11 - 10
examples/fullstack-auth/src/main.rs

@@ -75,7 +75,8 @@ fn app() -> Element {
         div {
             button { onclick: move |_| {
                     async move {
-                        login().await.unwrap();
+                        login().await?;
+                        Ok(())
                     }
                 },
                 "Login Test User"
@@ -84,9 +85,9 @@ fn app() -> Element {
         div {
             button {
                 onclick: move |_| async move {
-                    if let Ok(data) = get_user_name().await {
-                        user_name.set(data);
-                    }
+                    let data = get_user_name().await?;
+                    user_name.set(data);
+                    Ok(())
                 },
                 "Get User Name"
             }
@@ -95,9 +96,9 @@ fn app() -> Element {
         div {
             button {
                 onclick: move |_| async move {
-                    if let Ok(data) = get_permissions().await {
-                        permissions.set(data);
-                    }
+                    let data = get_permissions().await?;
+                    permissions.set(data);
+                    Ok(())
                 },
                 "Get Permissions"
             }
@@ -107,20 +108,20 @@ fn app() -> Element {
 }
 
 #[server]
-pub async fn get_user_name() -> Result<String, ServerFnError> {
+pub async fn get_user_name() -> ServerFnResult<String> {
     let auth = auth::get_session().await?;
     Ok(auth.current_user.unwrap().username.to_string())
 }
 
 #[server]
-pub async fn login() -> Result<(), ServerFnError> {
+pub async fn login() -> ServerFnResult {
     let auth = auth::get_session().await?;
     auth.login_user(2);
     Ok(())
 }
 
 #[server]
-pub async fn get_permissions() -> Result<String, ServerFnError> {
+pub async fn get_permissions() -> ServerFnResult<String> {
     let method: axum::http::Method = extract().await?;
     let auth = auth::get_session().await?;
     let current_user = auth.current_user.clone().unwrap_or_default();

+ 9 - 9
examples/fullstack-desktop/src/main.rs

@@ -18,11 +18,11 @@ pub fn app() -> Element {
         button { onclick: move |_| count -= 1, "Down low!" }
         button {
             onclick: move |_| async move {
-                if let Ok(data) = get_server_data().await {
-                    println!("Client received: {}", data);
-                    text.set(data.clone());
-                    post_server_data(data).await.unwrap();
-                }
+                let data = get_server_data().await?;
+                println!("Client received: {}", data);
+                text.set(data.clone());
+                post_server_data(data).await?;
+                Ok(())
             },
             "Run a server function"
         }
@@ -30,14 +30,14 @@ pub fn app() -> Element {
     }
 }
 
-#[server(PostServerData)]
-async fn post_server_data(data: String) -> Result<(), ServerFnError> {
+#[server]
+async fn post_server_data(data: String) -> ServerFnResult {
     println!("Server received: {}", data);
 
     Ok(())
 }
 
-#[server(GetServerData)]
-async fn get_server_data() -> Result<String, ServerFnError> {
+#[server]
+async fn get_server_data() -> ServerFnResult<String> {
     Ok("Hello from the server!".to_string())
 }

+ 7 - 7
examples/fullstack-hello-world/src/main.rs

@@ -20,11 +20,11 @@ fn app() -> Element {
         button { onclick: move |_| count -= 1, "Down low!" }
         button {
             onclick: move |_| async move {
-                if let Ok(data) = get_server_data().await {
-                    println!("Client received: {}", data);
-                    text.set(data.clone());
-                    post_server_data(data).await.unwrap();
-                }
+                let data = get_server_data().await?;
+                println!("Client received: {}", data);
+                text.set(data.clone());
+                post_server_data(data).await?;
+                Ok(())
             },
             "Run a server function!"
         }
@@ -33,14 +33,14 @@ fn app() -> Element {
 }
 
 #[server]
-async fn post_server_data(data: String) -> Result<(), ServerFnError> {
+async fn post_server_data(data: String) -> ServerFnResult {
     println!("Server received: {}", data);
 
     Ok(())
 }
 
 #[server]
-async fn get_server_data() -> Result<String, ServerFnError> {
+async fn get_server_data() -> ServerFnResult<String> {
     Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
 }
 

+ 17 - 17
examples/fullstack-router/src/main.rs

@@ -52,34 +52,34 @@ fn Home() -> Element {
     let mut text = use_signal(|| "...".to_string());
 
     rsx! {
-    Link { to: Route::Blog { id: count() }, "Go to blog" }
-    div {
-        h1 { "High-Five counter: {count}" }
-        button { onclick: move |_| count += 1, "Up high!" }
-        button { onclick: move |_| count -= 1, "Down low!" }
-        button {
-            onclick: move |_| async move {
-                if let Ok(data) = get_server_data().await {
+        Link { to: Route::Blog { id: count() }, "Go to blog" }
+        div {
+            h1 { "High-Five counter: {count}" }
+            button { onclick: move |_| count += 1, "Up high!" }
+            button { onclick: move |_| count -= 1, "Down low!" }
+            button {
+                onclick: move |_| async move {
+                    let data = get_server_data().await?;
                     println!("Client received: {}", data);
                     text.set(data.clone());
-                    post_server_data(data).await.unwrap();
-                }
-            },
-            "Run server function!"
+                    post_server_data(data).await?;
+                    Ok(())
+                },
+                "Run server function!"
+            }
+            "Server said: {text}"
         }
-        "Server said: {text}"
-                    }
-                }
+    }
 }
 
 #[server(PostServerData)]
-async fn post_server_data(data: String) -> Result<(), ServerFnError> {
+async fn post_server_data(data: String) -> ServerFnResult {
     println!("Server received: {}", data);
 
     Ok(())
 }
 
 #[server(GetServerData)]
-async fn get_server_data() -> Result<String, ServerFnError> {
+async fn get_server_data() -> ServerFnResult<String> {
     Ok("Hello from the server!".to_string())
 }

+ 0 - 1
examples/fullstack-streaming/Cargo.toml

@@ -12,7 +12,6 @@ serde = { workspace = true }
 futures = { workspace = true }
 tokio = { workspace = true, optional = true }
 futures-util.workspace = true
-once_cell = { workspace = true }
 
 [features]
 default = []

+ 7 - 7
examples/fullstack-streaming/src/main.rs

@@ -9,13 +9,13 @@ fn app() -> Element {
         button {
             onclick: move |_| async move {
                 response.write().clear();
-                if let Ok(stream) = test_stream().await {
-                    response.write().push_str("Stream started\n");
-                    let mut stream = stream.into_inner();
-                    while let Some(Ok(text)) = stream.next().await {
-                        response.write().push_str(&text);
-                    }
+                let stream = test_stream().await?;
+                response.write().push_str("Stream started\n");
+                let mut stream = stream.into_inner();
+                while let Some(Ok(text)) = stream.next().await {
+                    response.write().push_str(&text);
                 }
+                Ok(())
             },
             "Start stream"
         }
@@ -24,7 +24,7 @@ fn app() -> Element {
 }
 
 #[server(output = StreamingText)]
-pub async fn test_stream() -> Result<TextStream, ServerFnError> {
+pub async fn test_stream() -> ServerFnResult<TextStream<ServerFnError>> {
     let (tx, rx) = futures::channel::mpsc::unbounded();
     tokio::spawn(async move {
         loop {

+ 4 - 0
examples/fullstack-websockets/.gitignore

@@ -0,0 +1,4 @@
+dist
+target
+static
+.dioxus

+ 16 - 0
examples/fullstack-websockets/Cargo.toml

@@ -0,0 +1,16 @@
+[package]
+name = "fullstack-websocket-example"
+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 = ["fullstack"] }
+futures.workspace = true
+tokio = { workspace = true, features = ["full"], optional = true }
+
+[features]
+server = ["dioxus/server", "dep:tokio"]
+web = ["dioxus/web"]

+ 69 - 0
examples/fullstack-websockets/src/main.rs

@@ -0,0 +1,69 @@
+#![allow(non_snake_case)]
+use dioxus::prelude::{
+    server_fn::{codec::JsonEncoding, BoxedStream, Websocket},
+    *,
+};
+use futures::{channel::mpsc, SinkExt, StreamExt};
+
+fn main() {
+    launch(app);
+}
+
+fn app() -> Element {
+    let mut uppercase = use_signal(String::new);
+    let mut uppercase_channel = use_signal(|| None);
+
+    // Start the websocket connection in a background task
+    use_future(move || async move {
+        let (tx, rx) = mpsc::channel(1);
+        let mut receiver = uppercase_ws(rx.into()).await.unwrap();
+        // Store the channel in a signal for use in the input handler
+        uppercase_channel.set(Some(tx));
+        // Whenever we get a message from the server, update the uppercase signal
+        while let Some(Ok(msg)) = receiver.next().await {
+            uppercase.set(msg);
+        }
+    });
+
+    rsx! {
+        input {
+            oninput: move |e| async move {
+                if let Some(mut uppercase_channel) = uppercase_channel() {
+                    let msg = e.value();
+                    uppercase_channel.send(Ok(msg)).await.unwrap();
+                }
+            },
+        }
+        "Uppercase: {uppercase}"
+    }
+}
+
+// The server macro accepts a protocol parameter which implements the protocol trait. The protocol
+// controls how the inputs and outputs are encoded when handling the server function. In this case,
+// the websocket<json, json> protocol can encode a stream input and stream output where messages are
+// serialized as JSON
+#[server(protocol = Websocket<JsonEncoding, JsonEncoding>)]
+async fn uppercase_ws(
+    input: BoxedStream<String, ServerFnError>,
+) -> ServerFnResult<BoxedStream<String, ServerFnError>> {
+    let mut input = input;
+
+    // Create a channel with the output of the websocket
+    let (mut tx, rx) = mpsc::channel(1);
+
+    // Spawn a task that processes the input stream and sends any new messages to the output
+    tokio::spawn(async move {
+        while let Some(msg) = input.next().await {
+            if tx
+                .send(msg.map(|msg| msg.to_ascii_uppercase()))
+                .await
+                .is_err()
+            {
+                break;
+            }
+        }
+    });
+
+    // Return the output stream
+    Ok(rx.into())
+}

+ 16 - 5
examples/future.rs

@@ -13,7 +13,8 @@ fn main() {
 fn app() -> Element {
     let mut count = use_signal(|| 0);
 
-    // use_future will run the future
+    // use_future is a non-reactive hook that simply runs a future in the background.
+    // You can use the UseFuture handle to pause, resume, restart, or cancel the future.
     use_future(move || async move {
         loop {
             sleep(std::time::Duration::from_millis(200)).await;
@@ -21,7 +22,11 @@ fn app() -> Element {
         }
     });
 
-    // We can also spawn futures from effects, handlers, or other futures
+    // use_effect is a reactive hook that runs a future when signals captured by its reactive context
+    // are modified. This is similar to use_effect in React and is useful for running side effects
+    // that depend on the state of your component.
+    //
+    // Generally, we recommend performing async work in event as a reaction to a user event.
     use_effect(move || {
         spawn(async move {
             sleep(std::time::Duration::from_secs(5)).await;
@@ -29,10 +34,16 @@ fn app() -> Element {
         });
     });
 
+    // You can run futures directly from event handlers as well. Note that if the event handler is
+    // fired multiple times, the future will be spawned multiple times.
     rsx! {
-        div {
-            h1 { "Current count: {count}" }
-            button { onclick: move |_| count.set(0), "Reset the count" }
+        h1 { "Current count: {count}" }
+        button {
+            onclick: move |_| async move {
+                sleep(std::time::Duration::from_millis(200)).await;
+                count.set(0);
+            },
+            "Slowly reset the count"
         }
     }
 }

+ 2 - 0
examples/tailwind/README.md

@@ -5,3 +5,5 @@ This example shows how an app might be styled with TailwindCSS.
 ## Running
 
 Our [Tailwind](https://dioxuslabs.com/learn/0.6/cookbook/tailwind) guide explains how to setup and run Dioxus-Tailwind projects.
+
+Note that in Dioxus 0.7, the Tailwind watcher is initialized automatically if a `tailwind.css` file is find in your app's root.

+ 0 - 3
examples/tailwind/input.css

@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;

+ 431 - 799
examples/tailwind/public/tailwind.css

@@ -1,833 +1,465 @@
-/*
-! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
-  box-sizing: border-box;
-  /* 1 */
-  border-width: 0;
-  /* 2 */
-  border-style: solid;
-  /* 2 */
-  border-color: #e5e7eb;
-  /* 2 */
-}
-
-::before,
-::after {
-  --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-*/
-
-html {
-  line-height: 1.5;
-  /* 1 */
-  -webkit-text-size-adjust: 100%;
-  /* 2 */
-  -moz-tab-size: 4;
-  /* 3 */
-  -o-tab-size: 4;
-     tab-size: 4;
-  /* 3 */
-  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-  /* 4 */
-  font-feature-settings: normal;
-  /* 5 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
-  margin: 0;
-  /* 1 */
-  line-height: inherit;
-  /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
-  height: 0;
-  /* 1 */
-  color: inherit;
-  /* 2 */
-  border-top-width: 1px;
-  /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
-  -webkit-text-decoration: underline dotted;
-          text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  font-size: inherit;
-  font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
-  color: inherit;
-  text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
-  font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font family by default.
-2. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
-  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-  /* 1 */
-  font-size: 1em;
-  /* 2 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
-  font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
-  font-size: 75%;
-  line-height: 0;
-  position: relative;
-  vertical-align: baseline;
-}
-
-sub {
-  bottom: -0.25em;
-}
-
-sup {
-  top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
-  text-indent: 0;
-  /* 1 */
-  border-color: inherit;
-  /* 2 */
-  border-collapse: collapse;
-  /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
-  font-family: inherit;
-  /* 1 */
-  font-size: 100%;
-  /* 1 */
-  font-weight: inherit;
-  /* 1 */
-  line-height: inherit;
-  /* 1 */
-  color: inherit;
-  /* 1 */
-  margin: 0;
-  /* 2 */
-  padding: 0;
-  /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
-  text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-[type='button'],
-[type='reset'],
-[type='submit'] {
-  -webkit-appearance: button;
-  /* 1 */
-  background-color: transparent;
-  /* 2 */
-  background-image: none;
-  /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
-  outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
-  box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
-  vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
-  height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
-  -webkit-appearance: textfield;
-  /* 1 */
-  outline-offset: -2px;
-  /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
-  -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
-  -webkit-appearance: button;
-  /* 1 */
-  font: inherit;
-  /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
-  display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
-  margin: 0;
-}
-
-fieldset {
-  margin: 0;
-  padding: 0;
-}
-
-legend {
-  padding: 0;
-}
-
-ol,
-ul,
-menu {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
-  resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
-  opacity: 1;
-  /* 1 */
-  color: #9ca3af;
-  /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
-  cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
-  cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
-   This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
-  display: block;
-  /* 1 */
-  vertical-align: middle;
-  /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
-  max-width: 100%;
-  height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden] {
-  display: none;
-}
-
-*, ::before, ::after {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-}
-
-::backdrop {
-  --tw-border-spacing-x: 0;
-  --tw-border-spacing-y: 0;
-  --tw-translate-x: 0;
-  --tw-translate-y: 0;
-  --tw-rotate: 0;
-  --tw-skew-x: 0;
-  --tw-skew-y: 0;
-  --tw-scale-x: 1;
-  --tw-scale-y: 1;
-  --tw-pan-x:  ;
-  --tw-pan-y:  ;
-  --tw-pinch-zoom:  ;
-  --tw-scroll-snap-strictness: proximity;
-  --tw-ordinal:  ;
-  --tw-slashed-zero:  ;
-  --tw-numeric-figure:  ;
-  --tw-numeric-spacing:  ;
-  --tw-numeric-fraction:  ;
-  --tw-ring-inset:  ;
-  --tw-ring-offset-width: 0px;
-  --tw-ring-offset-color: #fff;
-  --tw-ring-color: rgb(59 130 246 / 0.5);
-  --tw-ring-offset-shadow: 0 0 #0000;
-  --tw-ring-shadow: 0 0 #0000;
-  --tw-shadow: 0 0 #0000;
-  --tw-shadow-colored: 0 0 #0000;
-  --tw-blur:  ;
-  --tw-brightness:  ;
-  --tw-contrast:  ;
-  --tw-grayscale:  ;
-  --tw-hue-rotate:  ;
-  --tw-invert:  ;
-  --tw-saturate:  ;
-  --tw-sepia:  ;
-  --tw-drop-shadow:  ;
-  --tw-backdrop-blur:  ;
-  --tw-backdrop-brightness:  ;
-  --tw-backdrop-contrast:  ;
-  --tw-backdrop-grayscale:  ;
-  --tw-backdrop-hue-rotate:  ;
-  --tw-backdrop-invert:  ;
-  --tw-backdrop-opacity:  ;
-  --tw-backdrop-saturate:  ;
-  --tw-backdrop-sepia:  ;
-}
-
-.container {
-  width: 100%;
-}
-
-@media (min-width: 640px) {
-  .container {
-    max-width: 640px;
+/*! tailwindcss v4.1.0 | MIT License | https://tailwindcss.com */
+@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
+  @layer base {
+    *, ::before, ::after, ::backdrop {
+      --tw-border-style: solid;
+      --tw-leading: initial;
+      --tw-font-weight: initial;
+    }
   }
 }
-
-@media (min-width: 768px) {
-  .container {
-    max-width: 768px;
+@layer theme, base, components, utilities;
+@layer theme {
+  :root, :host {
+    --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+      "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+      "Courier New", monospace;
+    --color-indigo-500: oklch(58.5% 0.233 277.117);
+    --color-indigo-600: oklch(51.1% 0.262 276.966);
+    --color-gray-400: oklch(70.7% 0.022 261.325);
+    --color-gray-700: oklch(37.3% 0.034 259.733);
+    --color-gray-800: oklch(27.8% 0.033 256.848);
+    --color-gray-900: oklch(21% 0.034 264.665);
+    --color-white: #fff;
+    --spacing: 0.25rem;
+    --container-lg: 32rem;
+    --text-base: 1rem;
+    --text-base--line-height: calc(1.5 / 1);
+    --text-lg: 1.125rem;
+    --text-lg--line-height: calc(1.75 / 1.125);
+    --text-xl: 1.25rem;
+    --text-xl--line-height: calc(1.75 / 1.25);
+    --text-3xl: 1.875rem;
+    --text-3xl--line-height: calc(2.25 / 1.875);
+    --text-4xl: 2.25rem;
+    --text-4xl--line-height: calc(2.5 / 2.25);
+    --font-weight-medium: 500;
+    --leading-relaxed: 1.625;
+    --radius-sm: 0.25rem;
+    --default-font-family: var(--font-sans);
+    --default-mono-font-family: var(--font-mono);
   }
 }
-
-@media (min-width: 1024px) {
-  .container {
-    max-width: 1024px;
+@layer base {
+  *, ::after, ::before, ::backdrop, ::file-selector-button {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    border: 0 solid;
   }
-}
-
-@media (min-width: 1280px) {
-  .container {
-    max-width: 1280px;
+  html, :host {
+    line-height: 1.5;
+    -webkit-text-size-adjust: 100%;
+    tab-size: 4;
+    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+    font-feature-settings: var(--default-font-feature-settings, normal);
+    font-variation-settings: var(--default-font-variation-settings, normal);
+    -webkit-tap-highlight-color: transparent;
+  }
+  hr {
+    height: 0;
+    color: inherit;
+    border-top-width: 1px;
+  }
+  abbr:where([title]) {
+    -webkit-text-decoration: underline dotted;
+    text-decoration: underline dotted;
+  }
+  h1, h2, h3, h4, h5, h6 {
+    font-size: inherit;
+    font-weight: inherit;
+  }
+  a {
+    color: inherit;
+    -webkit-text-decoration: inherit;
+    text-decoration: inherit;
+  }
+  b, strong {
+    font-weight: bolder;
+  }
+  code, kbd, samp, pre {
+    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
+    font-feature-settings: var(--default-mono-font-feature-settings, normal);
+    font-variation-settings: var(--default-mono-font-variation-settings, normal);
+    font-size: 1em;
+  }
+  small {
+    font-size: 80%;
+  }
+  sub, sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+  }
+  sub {
+    bottom: -0.25em;
+  }
+  sup {
+    top: -0.5em;
+  }
+  table {
+    text-indent: 0;
+    border-color: inherit;
+    border-collapse: collapse;
+  }
+  :-moz-focusring {
+    outline: auto;
+  }
+  progress {
+    vertical-align: baseline;
+  }
+  summary {
+    display: list-item;
+  }
+  ol, ul, menu {
+    list-style: none;
+  }
+  img, svg, video, canvas, audio, iframe, embed, object {
+    display: block;
+    vertical-align: middle;
+  }
+  img, video {
+    max-width: 100%;
+    height: auto;
+  }
+  button, input, select, optgroup, textarea, ::file-selector-button {
+    font: inherit;
+    font-feature-settings: inherit;
+    font-variation-settings: inherit;
+    letter-spacing: inherit;
+    color: inherit;
+    border-radius: 0;
+    background-color: transparent;
+    opacity: 1;
+  }
+  :where(select:is([multiple], [size])) optgroup {
+    font-weight: bolder;
+  }
+  :where(select:is([multiple], [size])) optgroup option {
+    padding-inline-start: 20px;
+  }
+  ::file-selector-button {
+    margin-inline-end: 4px;
+  }
+  ::placeholder {
+    opacity: 1;
+  }
+  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
+    ::placeholder {
+      color: color-mix(in oklab, currentColor 50%, transparent);
+    }
+  }
+  textarea {
+    resize: vertical;
+  }
+  ::-webkit-search-decoration {
+    -webkit-appearance: none;
+  }
+  ::-webkit-date-and-time-value {
+    min-height: 1lh;
+    text-align: inherit;
+  }
+  ::-webkit-datetime-edit {
+    display: inline-flex;
+  }
+  ::-webkit-datetime-edit-fields-wrapper {
+    padding: 0;
+  }
+  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
+    padding-block: 0;
+  }
+  :-moz-ui-invalid {
+    box-shadow: none;
+  }
+  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
+    appearance: button;
+  }
+  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
+    height: auto;
+  }
+  [hidden]:where(:not([hidden="until-found"])) {
+    display: none !important;
   }
 }
-
-@media (min-width: 1536px) {
+@layer utilities {
   .container {
-    max-width: 1536px;
+    width: 100%;
+    @media (width >= 40rem) {
+      max-width: 40rem;
+    }
+    @media (width >= 48rem) {
+      max-width: 48rem;
+    }
+    @media (width >= 64rem) {
+      max-width: 64rem;
+    }
+    @media (width >= 80rem) {
+      max-width: 80rem;
+    }
+    @media (width >= 96rem) {
+      max-width: 96rem;
+    }
+  }
+  .mx-auto {
+    margin-inline: auto;
+  }
+  .mt-4 {
+    margin-top: calc(var(--spacing) * 4);
+  }
+  .mr-5 {
+    margin-right: calc(var(--spacing) * 5);
+  }
+  .mb-4 {
+    margin-bottom: calc(var(--spacing) * 4);
+  }
+  .mb-8 {
+    margin-bottom: calc(var(--spacing) * 8);
+  }
+  .mb-16 {
+    margin-bottom: calc(var(--spacing) * 16);
+  }
+  .ml-1 {
+    margin-left: calc(var(--spacing) * 1);
+  }
+  .ml-3 {
+    margin-left: calc(var(--spacing) * 3);
+  }
+  .ml-4 {
+    margin-left: calc(var(--spacing) * 4);
+  }
+  .flex {
+    display: flex;
+  }
+  .hidden {
+    display: none;
+  }
+  .inline-flex {
+    display: inline-flex;
+  }
+  .h-4 {
+    height: calc(var(--spacing) * 4);
+  }
+  .h-10 {
+    height: calc(var(--spacing) * 10);
+  }
+  .w-4 {
+    width: calc(var(--spacing) * 4);
+  }
+  .w-5\/6 {
+    width: calc(5/6 * 100%);
+  }
+  .w-10 {
+    width: calc(var(--spacing) * 10);
+  }
+  .flex-col {
+    flex-direction: column;
+  }
+  .flex-wrap {
+    flex-wrap: wrap;
+  }
+  .items-center {
+    align-items: center;
+  }
+  .justify-center {
+    justify-content: center;
+  }
+  .rounded-full {
+    border-radius: calc(infinity * 1px);
+  }
+  .rounded-sm {
+    border-radius: var(--radius-sm);
+  }
+  .border-0 {
+    border-style: var(--tw-border-style);
+    border-width: 0px;
+  }
+  .bg-gray-800 {
+    background-color: var(--color-gray-800);
+  }
+  .bg-gray-900 {
+    background-color: var(--color-gray-900);
+  }
+  .bg-indigo-500 {
+    background-color: var(--color-indigo-500);
+  }
+  .object-cover {
+    object-fit: cover;
+  }
+  .object-center {
+    object-position: center;
+  }
+  .p-2 {
+    padding: calc(var(--spacing) * 2);
+  }
+  .p-5 {
+    padding: calc(var(--spacing) * 5);
+  }
+  .px-3 {
+    padding-inline: calc(var(--spacing) * 3);
+  }
+  .px-5 {
+    padding-inline: calc(var(--spacing) * 5);
+  }
+  .px-6 {
+    padding-inline: calc(var(--spacing) * 6);
+  }
+  .py-1 {
+    padding-block: calc(var(--spacing) * 1);
+  }
+  .py-2 {
+    padding-block: calc(var(--spacing) * 2);
+  }
+  .py-24 {
+    padding-block: calc(var(--spacing) * 24);
+  }
+  .text-center {
+    text-align: center;
+  }
+  .text-3xl {
+    font-size: var(--text-3xl);
+    line-height: var(--tw-leading, var(--text-3xl--line-height));
+  }
+  .text-base {
+    font-size: var(--text-base);
+    line-height: var(--tw-leading, var(--text-base--line-height));
+  }
+  .text-lg {
+    font-size: var(--text-lg);
+    line-height: var(--tw-leading, var(--text-lg--line-height));
+  }
+  .text-xl {
+    font-size: var(--text-xl);
+    line-height: var(--tw-leading, var(--text-xl--line-height));
+  }
+  .leading-relaxed {
+    --tw-leading: var(--leading-relaxed);
+    line-height: var(--leading-relaxed);
+  }
+  .font-medium {
+    --tw-font-weight: var(--font-weight-medium);
+    font-weight: var(--font-weight-medium);
+  }
+  .text-gray-400 {
+    color: var(--color-gray-400);
+  }
+  .text-white {
+    color: var(--color-white);
+  }
+  .hover\:bg-gray-700 {
+    &:hover {
+      @media (hover: hover) {
+        background-color: var(--color-gray-700);
+      }
+    }
+  }
+  .hover\:bg-indigo-600 {
+    &:hover {
+      @media (hover: hover) {
+        background-color: var(--color-indigo-600);
+      }
+    }
+  }
+  .hover\:text-white {
+    &:hover {
+      @media (hover: hover) {
+        color: var(--color-white);
+      }
+    }
+  }
+  .focus\:outline-hidden {
+    &:focus {
+      --tw-outline-style: none;
+      outline-style: none;
+      @media (forced-colors: active) {
+        outline: 2px solid transparent;
+        outline-offset: 2px;
+      }
+    }
   }
-}
-
-.mx-auto {
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.mb-16 {
-  margin-bottom: 4rem;
-}
-
-.mb-4 {
-  margin-bottom: 1rem;
-}
-
-.mb-8 {
-  margin-bottom: 2rem;
-}
-
-.ml-1 {
-  margin-left: 0.25rem;
-}
-
-.ml-3 {
-  margin-left: 0.75rem;
-}
-
-.ml-4 {
-  margin-left: 1rem;
-}
-
-.mr-5 {
-  margin-right: 1.25rem;
-}
-
-.mt-4 {
-  margin-top: 1rem;
-}
-
-.flex {
-  display: flex;
-}
-
-.inline-flex {
-  display: inline-flex;
-}
-
-.hidden {
-  display: none;
-}
-
-.h-10 {
-  height: 2.5rem;
-}
-
-.h-4 {
-  height: 1rem;
-}
-
-.w-10 {
-  width: 2.5rem;
-}
-
-.w-4 {
-  width: 1rem;
-}
-
-.w-5\/6 {
-  width: 83.333333%;
-}
-
-.flex-col {
-  flex-direction: column;
-}
-
-.flex-wrap {
-  flex-wrap: wrap;
-}
-
-.items-center {
-  align-items: center;
-}
-
-.justify-center {
-  justify-content: center;
-}
-
-.rounded {
-  border-radius: 0.25rem;
-}
-
-.rounded-full {
-  border-radius: 9999px;
-}
-
-.border-0 {
-  border-width: 0px;
-}
-
-.bg-gray-800 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(31 41 55 / var(--tw-bg-opacity));
-}
-
-.bg-gray-900 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(17 24 39 / var(--tw-bg-opacity));
-}
-
-.bg-indigo-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(99 102 241 / var(--tw-bg-opacity));
-}
-
-.object-cover {
-  -o-object-fit: cover;
-     object-fit: cover;
-}
-
-.object-center {
-  -o-object-position: center;
-     object-position: center;
-}
-
-.p-2 {
-  padding: 0.5rem;
-}
-
-.p-5 {
-  padding: 1.25rem;
-}
-
-.px-3 {
-  padding-left: 0.75rem;
-  padding-right: 0.75rem;
-}
-
-.px-5 {
-  padding-left: 1.25rem;
-  padding-right: 1.25rem;
-}
-
-.px-6 {
-  padding-left: 1.5rem;
-  padding-right: 1.5rem;
-}
-
-.py-1 {
-  padding-top: 0.25rem;
-  padding-bottom: 0.25rem;
-}
-
-.py-2 {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-}
-
-.py-24 {
-  padding-top: 6rem;
-  padding-bottom: 6rem;
-}
-
-.text-center {
-  text-align: center;
-}
-
-.text-3xl {
-  font-size: 1.875rem;
-  line-height: 2.25rem;
-}
-
-.text-base {
-  font-size: 1rem;
-  line-height: 1.5rem;
-}
-
-.text-lg {
-  font-size: 1.125rem;
-  line-height: 1.75rem;
-}
-
-.text-xl {
-  font-size: 1.25rem;
-  line-height: 1.75rem;
-}
-
-.font-medium {
-  font-weight: 500;
-}
-
-.leading-relaxed {
-  line-height: 1.625;
-}
-
-.text-gray-400 {
-  --tw-text-opacity: 1;
-  color: rgb(156 163 175 / var(--tw-text-opacity));
-}
-
-.text-white {
-  --tw-text-opacity: 1;
-  color: rgb(255 255 255 / var(--tw-text-opacity));
-}
-
-.hover\:bg-gray-700:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(55 65 81 / var(--tw-bg-opacity));
-}
-
-.hover\:bg-indigo-600:hover {
-  --tw-bg-opacity: 1;
-  background-color: rgb(79 70 229 / var(--tw-bg-opacity));
-}
-
-.hover\:text-white:hover {
-  --tw-text-opacity: 1;
-  color: rgb(255 255 255 / var(--tw-text-opacity));
-}
-
-.focus\:outline-none:focus {
-  outline: 2px solid transparent;
-  outline-offset: 2px;
-}
-
-@media (min-width: 640px) {
   .sm\:text-4xl {
-    font-size: 2.25rem;
-    line-height: 2.5rem;
+    @media (width >= 40rem) {
+      font-size: var(--text-4xl);
+      line-height: var(--tw-leading, var(--text-4xl--line-height));
+    }
+  }
+  .md\:mt-0 {
+    @media (width >= 48rem) {
+      margin-top: calc(var(--spacing) * 0);
+    }
   }
-}
-
-@media (min-width: 768px) {
   .md\:mb-0 {
-    margin-bottom: 0px;
+    @media (width >= 48rem) {
+      margin-bottom: calc(var(--spacing) * 0);
+    }
   }
-
   .md\:ml-auto {
-    margin-left: auto;
-  }
-
-  .md\:mt-0 {
-    margin-top: 0px;
+    @media (width >= 48rem) {
+      margin-left: auto;
+    }
   }
-
   .md\:w-1\/2 {
-    width: 50%;
+    @media (width >= 48rem) {
+      width: calc(1/2 * 100%);
+    }
   }
-
   .md\:flex-row {
-    flex-direction: row;
+    @media (width >= 48rem) {
+      flex-direction: row;
+    }
   }
-
   .md\:items-start {
-    align-items: flex-start;
+    @media (width >= 48rem) {
+      align-items: flex-start;
+    }
   }
-
   .md\:pr-16 {
-    padding-right: 4rem;
+    @media (width >= 48rem) {
+      padding-right: calc(var(--spacing) * 16);
+    }
   }
-
   .md\:text-left {
-    text-align: left;
+    @media (width >= 48rem) {
+      text-align: left;
+    }
   }
-}
-
-@media (min-width: 1024px) {
   .lg\:inline-block {
-    display: inline-block;
+    @media (width >= 64rem) {
+      display: inline-block;
+    }
   }
-
   .lg\:w-full {
-    width: 100%;
+    @media (width >= 64rem) {
+      width: 100%;
+    }
   }
-
   .lg\:max-w-lg {
-    max-width: 32rem;
+    @media (width >= 64rem) {
+      max-width: var(--container-lg);
+    }
   }
-
-  .lg\:flex-grow {
-    flex-grow: 1;
+  .lg\:grow {
+    @media (width >= 64rem) {
+      flex-grow: 1;
+    }
   }
-
   .lg\:pr-24 {
-    padding-right: 6rem;
+    @media (width >= 64rem) {
+      padding-right: calc(var(--spacing) * 24);
+    }
   }
-}
+}
+@property --tw-border-style {
+  syntax: "*";
+  inherits: false;
+  initial-value: solid;
+}
+@property --tw-leading {
+  syntax: "*";
+  inherits: false;
+}
+@property --tw-font-weight {
+  syntax: "*";
+  inherits: false;
+}

+ 5 - 5
examples/tailwind/src/main.rs

@@ -25,7 +25,7 @@ fn app() -> Element {
                         a { class: "mr-5 hover:text-white", "Third Link" }
                         a { class: "mr-5 hover:text-white", "Fourth Link" }
                     }
-                    button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
+                    button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-hidden hover:bg-gray-700 rounded-sm text-base mt-4 md:mt-0",
                         "Button"
                         RightArrowIcon {}
                     }
@@ -34,7 +34,7 @@ fn app() -> Element {
 
             section { class: "text-gray-400 bg-gray-900 body-font",
                 div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center",
-                    div { class: "lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center",
+                    div { class: "lg:grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center",
                         h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white",
                             br { class: "hidden lg:inline-block" }
                             "Dioxus Sneak Peek"
@@ -46,17 +46,17 @@ fn app() -> Element {
                             on mobile and embedded platforms."
                         }
                         div { class: "flex justify-center",
-                            button { class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg",
+                            button { class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-hidden hover:bg-indigo-600 rounded-sm text-lg",
                                 "Learn more"
                             }
-                            button { class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-none hover:bg-gray-700 hover:text-white rounded text-lg",
+                            button { class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-hidden hover:bg-gray-700 hover:text-white rounded-sm text-lg",
                                 "Build an app"
                             }
                         }
                     }
                     div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6",
                         img {
-                            class: "object-cover object-center rounded",
+                            class: "object-cover object-center rounded-sm",
                             src: "https://i.imgur.com/oK6BLtw.png",
                             referrerpolicy: "no-referrer",
                             alt: "hero",

+ 0 - 9
examples/tailwind/tailwind.config.js

@@ -1,9 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
-  mode: "all",
-  content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"],
-  theme: {
-    extend: {},
-  },
-  plugins: [],
-};

+ 2 - 0
examples/tailwind/tailwind.css

@@ -0,0 +1,2 @@
+@import "tailwindcss";
+@source "./src/**/*.{rs,html,css}";

+ 0 - 269
examples/todomvc-native.rs

@@ -1,269 +0,0 @@
-//! The typical TodoMVC app, implemented in Dioxus.
-
-use dioxus::prelude::*;
-use std::collections::HashMap;
-
-const STYLE: Asset = asset!("/examples/assets/todomvc-native.css");
-
-fn main() {
-    dioxus::launch(app);
-}
-
-#[derive(PartialEq, Eq, Clone, Copy)]
-enum FilterState {
-    All,
-    Active,
-    Completed,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-struct TodoItem {
-    id: u32,
-    checked: bool,
-    contents: String,
-}
-
-fn app() -> Element {
-    // We store the todos in a HashMap in a Signal.
-    // Each key is the id of the todo, and the value is the todo itself.
-    let mut todos = use_signal(HashMap::<u32, TodoItem>::new);
-
-    let filter = use_signal(|| FilterState::All);
-
-    // We use a simple memoized signal to calculate the number of active todos.
-    // Whenever the todos change, the active_todo_count will be recalculated.
-    let active_todo_count =
-        use_memo(move || todos.read().values().filter(|item| !item.checked).count());
-
-    // We use a memoized signal to filter the todos based on the current filter state.
-    // Whenever the todos or filter change, the filtered_todos will be recalculated.
-    // Note that we're only storing the IDs of the todos, not the todos themselves.
-    let filtered_todos = use_memo(move || {
-        let mut filtered_todos = todos
-            .read()
-            .iter()
-            .filter(|(_, item)| match filter() {
-                FilterState::All => true,
-                FilterState::Active => !item.checked,
-                FilterState::Completed => item.checked,
-            })
-            .map(|f| *f.0)
-            .collect::<Vec<_>>();
-
-        filtered_todos.sort_unstable();
-
-        filtered_todos
-    });
-
-    // Toggle all the todos to the opposite of the current state.
-    // If all todos are checked, uncheck them all. If any are unchecked, check them all.
-    let toggle_all = move |_| {
-        let check = active_todo_count() != 0;
-        for (_, item) in todos.write().iter_mut() {
-            item.checked = check;
-        }
-    };
-
-    rsx! {
-        document::Link { rel: "stylesheet", href: STYLE }
-        body {
-            section { class: "todoapp",
-                TodoHeader { todos }
-                section { class: "main",
-                    if !todos.read().is_empty() {
-                        input {
-                            id: "toggle-all",
-                            class: "toggle-all",
-                            r#type: "checkbox",
-                            onchange: toggle_all,
-                            checked: active_todo_count() == 0
-                        }
-                        label { r#for: "toggle-all" }
-                    }
-
-                    // Render the todos using the filtered_todos signal
-                    // We pass the ID into the TodoEntry component so it can access the todo from the todos signal.
-                    // Since we store the todos in a signal too, we also need to send down the todo list
-                    ul { class: "todo-list",
-                        for id in filtered_todos() {
-                            TodoEntry { key: "{id}", id, todos }
-                        }
-                    }
-
-                    // We only show the footer if there are todos.
-                    if !todos.read().is_empty() {
-                        ListFooter { active_todo_count, todos, filter }
-                    }
-                }
-            }
-
-            // A simple info footer
-            footer { class: "info",
-                p { "Double-click to edit a todo" }
-                p {
-                    "Created by "
-                    a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
-                }
-                p {
-                    "Part of "
-                    a { href: "http://todomvc.com", "TodoMVC" }
-                }
-            }
-        }
-    }
-}
-
-#[component]
-fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
-    let mut draft = use_signal(|| "".to_string());
-    let mut todo_id = use_signal(|| 0);
-
-    let onkeydown = move |evt: KeyboardEvent| {
-        if evt.key() == Key::Enter && !draft.read().is_empty() {
-            let id = todo_id();
-            let todo = TodoItem {
-                id,
-                checked: false,
-                contents: draft.to_string(),
-            };
-            todos.write().insert(id, todo);
-            todo_id += 1;
-            draft.set("".to_string());
-            evt.prevent_default();
-        }
-    };
-
-    rsx! {
-        header { class: "header",
-            h1 { "todos" }
-            input {
-                class: "new-todo",
-                r#type: "text",
-                placeholder: "What needs to be done?",
-                value: "{draft}",
-                autofocus: "true",
-                oninput: move |evt| draft.set(evt.value()),
-                onkeydown
-            }
-        }
-    }
-}
-
-/// A single todo entry
-/// This takes the ID of the todo and the todos signal as props
-/// We can use these together to memoize the todo contents and checked state
-#[component]
-fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, id: u32) -> Element {
-    let mut is_editing = use_signal(|| false);
-
-    // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos
-    // This way, the component will only re-render when the contents of the todo change, or when the editing state changes.
-    // This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering
-    let checked = use_memo(move || todos.read().get(&id).unwrap().checked);
-    let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone());
-
-    rsx! {
-        li {
-            // Dioxus lets you use if statements in rsx to conditionally render attributes
-            // These will get merged into a single class attribute
-            class: if checked() { "completed" },
-            class: if is_editing() { "editing" },
-
-            // Some basic controls for the todo
-            div { class: "view",
-                input {
-                    class: "toggle",
-                    r#type: "checkbox",
-                    id: "cbg-{id}",
-                    checked: "{checked}",
-                    oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked()
-                }
-                label {
-                    r#for: "cbg-{id}",
-                    onclick: move |evt| {
-                        is_editing.set(true);
-                        evt.prevent_default()
-                    },
-                    "{contents}"
-                }
-                button {
-                    class: "destroy",
-                    onclick: move |evt| {
-                        todos.write().remove(&id);
-                        evt.prevent_default();
-                    },
-                }
-            }
-
-            // Only render the actual input if we're editing
-            if is_editing() {
-                input {
-                    class: "edit",
-                    r#type: "text",
-                    value: "{contents}",
-                    oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(),
-                    autofocus: "true",
-                    onfocusout: move |_| is_editing.set(false),
-                    onkeydown: move |evt| {
-                        if matches!(evt.key(), Key::Enter | Key::Escape | Key::Tab) {
-                            evt.prevent_default();
-                            is_editing.set(false);
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-#[component]
-fn ListFooter(
-    mut todos: Signal<HashMap<u32, TodoItem>>,
-    active_todo_count: ReadOnlySignal<usize>,
-    mut filter: Signal<FilterState>,
-) -> Element {
-    // We use a memoized signal to calculate whether we should show the "Clear completed" button.
-    // This will recompute whenever the todos change, and if the value is true, the button will be shown.
-    let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked));
-
-    rsx! {
-        footer { class: "footer",
-            span { class: "todo-count",
-                strong { "{active_todo_count} " }
-                span {
-                    match active_todo_count() {
-                        1 => "item",
-                        _ => "items",
-                    },
-                    " left"
-                }
-            }
-            ul { class: "filters",
-                for (state , state_text , url) in [
-                    (FilterState::All, "All", "#/"),
-                    (FilterState::Active, "Active", "#/active"),
-                    (FilterState::Completed, "Completed", "#/completed"),
-                ] {
-                    li {
-                        a {
-                            href: url,
-                            class: if filter() == state { "selected" },
-                            onclick: move |evt| {
-                                filter.set(state);
-                                evt.prevent_default();
-                            },
-                            {state_text}
-                        }
-                    }
-                }
-            }
-            if show_clear_completed() {
-                button {
-                    class: "clear-completed",
-                    onclick: move |_| todos.write().retain(|_, todo| !todo.checked),
-                    "Clear completed"
-                }
-            }
-        }
-    }
-}

+ 23 - 0
examples/wgpu-texture/Cargo.toml

@@ -0,0 +1,23 @@
+# Copyright © SixtyFPS GmbH <info@slint.dev>
+# SPDX-License-Identifier: MIT
+
+[package]
+name = "wgpu-texture"
+version = "0.0.0"
+edition = "2021"
+license = "MIT"
+publish = false
+
+[features]
+default = ["desktop"]
+desktop = ["dioxus/desktop"]
+native = ["dioxus/native"]
+tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
+
+[dependencies]
+dioxus-native = { path = "../../packages/native" }
+dioxus = { workspace = true }
+wgpu = "24"
+bytemuck = "1"
+color = "0.3"
+tracing-subscriber = { workspace = true, optional = true }

+ 254 - 0
examples/wgpu-texture/src/demo_renderer.rs

@@ -0,0 +1,254 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+use crate::Color;
+use dioxus_native::{CustomPaintCtx, CustomPaintSource, TextureHandle};
+use std::sync::mpsc::{channel, Receiver, Sender};
+use std::time::Instant;
+use wgpu::{Device, Queue};
+
+pub struct DemoPaintSource {
+    state: DemoRendererState,
+    start_time: std::time::Instant,
+    tx: Sender<DemoMessage>,
+    rx: Receiver<DemoMessage>,
+    color: Color,
+}
+
+impl CustomPaintSource for DemoPaintSource {
+    fn resume(&mut self, device: &Device, queue: &Queue) {
+        // TODO: work out what to do about width/height
+        let active_state = ActiveDemoRenderer::new(device, queue);
+        self.state = DemoRendererState::Active(Box::new(active_state));
+    }
+
+    fn suspend(&mut self) {
+        self.state = DemoRendererState::Suspended;
+    }
+
+    fn render(
+        &mut self,
+        ctx: CustomPaintCtx<'_>,
+        width: u32,
+        height: u32,
+        _scale: f64,
+    ) -> Option<TextureHandle> {
+        self.process_messages();
+        self.render(ctx, width, height)
+    }
+}
+
+pub enum DemoMessage {
+    // Color in RGB format
+    SetColor(Color),
+}
+
+enum DemoRendererState {
+    Active(Box<ActiveDemoRenderer>),
+    Suspended,
+}
+
+#[derive(Clone)]
+struct TextureAndHandle {
+    texture: wgpu::Texture,
+    handle: TextureHandle,
+}
+
+struct ActiveDemoRenderer {
+    device: wgpu::Device,
+    queue: wgpu::Queue,
+    pipeline: wgpu::RenderPipeline,
+    displayed_texture: Option<TextureAndHandle>,
+    next_texture: Option<TextureAndHandle>,
+}
+
+impl DemoPaintSource {
+    pub fn new() -> Self {
+        let (tx, rx) = channel();
+        Self::with_channel(tx, rx)
+    }
+
+    pub fn with_channel(tx: Sender<DemoMessage>, rx: Receiver<DemoMessage>) -> Self {
+        Self {
+            state: DemoRendererState::Suspended,
+            start_time: std::time::Instant::now(),
+            tx,
+            rx,
+            color: Color::WHITE,
+        }
+    }
+
+    pub fn sender(&self) -> Sender<DemoMessage> {
+        self.tx.clone()
+    }
+
+    fn process_messages(&mut self) {
+        loop {
+            match self.rx.try_recv() {
+                Err(_) => return,
+                Ok(msg) => match msg {
+                    DemoMessage::SetColor(color) => self.color = color,
+                },
+            }
+        }
+    }
+
+    fn render(
+        &mut self,
+        ctx: CustomPaintCtx<'_>,
+        width: u32,
+        height: u32,
+    ) -> Option<TextureHandle> {
+        if width == 0 || height == 0 {
+            return None;
+        }
+        let DemoRendererState::Active(state) = &mut self.state else {
+            return None;
+        };
+
+        state.render(ctx, self.color.components, width, height, &self.start_time)
+    }
+}
+
+impl ActiveDemoRenderer {
+    pub(crate) fn new(device: &Device, queue: &Queue) -> Self {
+        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: None,
+            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
+                "shader.wgsl"
+            ))),
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: None,
+            bind_group_layouts: &[],
+            push_constant_ranges: &[wgpu::PushConstantRange {
+                stages: wgpu::ShaderStages::FRAGMENT,
+                range: 0..16, // full size in bytes, aligned
+            }],
+        });
+
+        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: None,
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                buffers: &[],
+                compilation_options: Default::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                compilation_options: Default::default(),
+                targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
+            }),
+            primitive: wgpu::PrimitiveState::default(),
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState::default(),
+            multiview: None,
+            cache: None,
+        });
+
+        Self {
+            device: device.clone(),
+            queue: queue.clone(),
+            pipeline,
+            displayed_texture: None,
+            next_texture: None,
+        }
+    }
+
+    pub(crate) fn render(
+        &mut self,
+        mut ctx: CustomPaintCtx<'_>,
+        light: [f32; 3],
+        width: u32,
+        height: u32,
+        start_time: &Instant,
+    ) -> Option<TextureHandle> {
+        // If "next texture" size doesn't match specified size then unregister and drop texture
+        if let Some(next) = &self.next_texture {
+            if next.texture.width() != width || next.texture.height() != height {
+                ctx.unregister_texture(next.handle);
+                self.next_texture = None;
+            }
+        }
+
+        // If there is no "next texture" then create one and register it.
+        let texture_and_handle = match &self.next_texture {
+            Some(next) => next,
+            None => {
+                let texture = create_texture(&self.device, width, height);
+                let handle = ctx.register_texture(texture.clone());
+                self.next_texture = Some(TextureAndHandle { texture, handle });
+                self.next_texture.as_ref().unwrap()
+            }
+        };
+
+        let next_texture = &texture_and_handle.texture;
+        let next_texture_handle = texture_and_handle.handle;
+
+        let elapsed: f32 = start_time.elapsed().as_millis() as f32 / 500.;
+        let [light_red, light_green, light_blue] = light;
+        let push_constants = PushConstants {
+            light_color_and_time: [light_red, light_green, light_blue, elapsed],
+        };
+
+        let mut encoder = self
+            .device
+            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
+        {
+            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: None,
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: &next_texture.create_view(&wgpu::TextureViewDescriptor::default()),
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+            rpass.set_pipeline(&self.pipeline);
+            rpass.set_push_constants(
+                wgpu::ShaderStages::FRAGMENT, // Stage (your constants are for fragment shader)
+                0,                            // Offset in bytes (start at 0)
+                bytemuck::bytes_of(&push_constants),
+            );
+            rpass.draw(0..3, 0..1);
+        }
+
+        self.queue.submit(Some(encoder.finish()));
+
+        std::mem::swap(&mut self.next_texture, &mut self.displayed_texture);
+        Some(next_texture_handle)
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+struct PushConstants {
+    light_color_and_time: [f32; 4],
+}
+
+fn create_texture(device: &wgpu::Device, width: u32, height: u32) -> wgpu::Texture {
+    device.create_texture(&wgpu::TextureDescriptor {
+        label: None,
+        size: wgpu::Extent3d {
+            width,
+            height,
+            depth_or_array_layers: 1,
+        },
+        mip_level_count: 1,
+        sample_count: 1,
+        dimension: wgpu::TextureDimension::D2,
+        format: wgpu::TextureFormat::Rgba8Unorm,
+        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+            | wgpu::TextureUsages::TEXTURE_BINDING
+            | wgpu::TextureUsages::COPY_SRC,
+        view_formats: &[],
+    })
+}

+ 109 - 0
examples/wgpu-texture/src/main.rs

@@ -0,0 +1,109 @@
+use color::{palette::css::WHITE, parse_color};
+use color::{OpaqueColor, Srgb};
+use demo_renderer::{DemoMessage, DemoPaintSource};
+use dioxus::prelude::*;
+use dioxus_native::use_wgpu;
+use std::any::Any;
+use wgpu::{Features, Limits};
+
+mod demo_renderer;
+
+// CSS Styles
+static STYLES: Asset = asset!("./src/styles.css");
+
+// WGPU settings required by this example
+const FEATURES: Features = Features::PUSH_CONSTANTS;
+fn limits() -> Limits {
+    Limits {
+        max_push_constant_size: 16,
+        ..Limits::default()
+    }
+}
+
+type Color = OpaqueColor<Srgb>;
+
+fn main() {
+    #[cfg(feature = "tracing")]
+    tracing_subscriber::fmt::init();
+
+    let config: Vec<Box<dyn Any>> = vec![Box::new(FEATURES), Box::new(limits())];
+    dioxus_native::launch_cfg(app, Vec::new(), config);
+}
+
+fn app() -> Element {
+    let mut show_cube = use_signal(|| true);
+
+    let color_str = use_signal(|| String::from("red"));
+    let color = use_memo(move || {
+        parse_color(&color_str())
+            .map(|c| c.to_alpha_color())
+            .unwrap_or(WHITE)
+            .split()
+            .0
+    });
+
+    use_effect(move || println!("{:?}", color().components));
+
+    rsx!(
+        document::Link { rel: "stylesheet", href: STYLES }
+        div { id:"overlay",
+            h2 { "Control Panel" },
+            button {
+                onclick: move |_| show_cube.toggle(),
+                if show_cube() {
+                    "Hide cube"
+                } else {
+                    "Show cube"
+                }
+            }
+            br {}
+            ColorControl { label: "Color:", color_str },
+            p { "This overlay demonstrates that the custom WGPU content can be rendered beneath layers of HTML content" }
+        }
+        div { id:"underlay",
+            h2 { "Underlay" },
+            p { "This underlay demonstrates that the custom WGPU content can be rendered above layers and blended with the content underneath" }
+        }
+        header {
+            h1 { "Blitz WGPU Demo" }
+        }
+        if show_cube() {
+            SpinningCube { color }
+        }
+    )
+}
+
+#[component]
+fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element {
+    rsx!(div {
+        class: "color-control",
+        { label },
+        input {
+            value: color_str(),
+            oninput: move |evt| {
+                *color_str.write() = evt.value()
+            }
+        }
+    })
+}
+
+#[component]
+fn SpinningCube(color: Memo<Color>) -> Element {
+    // Create custom paint source and register it with the renderer
+    let paint_source = DemoPaintSource::new();
+    let sender = paint_source.sender();
+    let paint_source_id = use_wgpu(move || paint_source);
+
+    use_effect(move || {
+        sender.send(DemoMessage::SetColor(color())).unwrap();
+    });
+
+    rsx!(
+        div { id:"canvas-container",
+            canvas {
+                id: "demo-canvas",
+                "src": paint_source_id
+            }
+        }
+    )
+}

+ 111 - 0
examples/wgpu-texture/src/shader.wgsl

@@ -0,0 +1,111 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+struct VertexOutput {
+    @builtin(position) position: vec4<f32>,
+    @location(0) frag_position: vec2<f32>,
+};
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) vertex_index: u32
+) -> VertexOutput {
+    var output: VertexOutput;
+
+    var positions = array<vec2<f32>, 3>(
+        vec2<f32>(-1.0,  3.0),
+        vec2<f32>(-1.0, -1.0),
+        vec2<f32>( 3.0, -1.0)
+    );
+
+    let pos = positions[vertex_index];
+    output.position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
+    output.frag_position = pos;
+    return output;
+}
+
+struct PushConstants {
+    light_color_and_time: vec4<f32>,
+};
+
+var<push_constant> pc: PushConstants;
+
+fn sdRoundBox(p: vec3<f32>, b: vec3<f32>, r: f32) -> f32 {
+    let q = abs(p) - b;
+    return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
+}
+
+fn rotateY(r: vec3<f32>, angle: f32) -> vec3<f32> {
+    let c = cos(angle);
+    let s = sin(angle);
+    let rotation_matrix = mat3x3<f32>(
+        vec3<f32>( c, 0.0,  s),
+        vec3<f32>(0.0, 1.0, 0.0),
+        vec3<f32>(-s, 0.0,  c)
+    );
+    return rotation_matrix * r;
+}
+
+fn rotateZ(r: vec3<f32>, angle: f32) -> vec3<f32> {
+    let c = cos(angle);
+    let s = sin(angle);
+    let rotation_matrix = mat3x3<f32>(
+        vec3<f32>( c, -s, 0.0),
+        vec3<f32>( s,  c, 0.0),
+        vec3<f32>(0.0, 0.0, 1.0)
+    );
+    return rotation_matrix * r;
+}
+
+// Distance from the scene
+fn scene(r: vec3<f32>) -> f32 {
+    let iTime = pc.light_color_and_time.w;
+    let pos = rotateZ(rotateY(r + vec3<f32>(-1.0, -1.0, 4.0), iTime), iTime);
+    let cube = vec3<f32>(0.5, 0.5, 0.5);
+    let edge = 0.1;
+    return sdRoundBox(pos, cube, edge);
+}
+
+// https://iquilezles.org/articles/normalsSDF
+fn normal(pos: vec3<f32>) -> vec3<f32> {
+    let e = vec2<f32>(1.0, -1.0) * 0.5773;
+    let eps = 0.0005;
+    return normalize(
+        e.xyy * scene(pos + e.xyy * eps) +
+        e.yyx * scene(pos + e.yyx * eps) +
+        e.yxy * scene(pos + e.yxy * eps) +
+        e.xxx * scene(pos + e.xxx * eps)
+    );
+}
+
+fn render(fragCoord: vec2<f32>, light_color: vec3<f32>) -> vec4<f32> {
+    var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+
+    var camera = vec3<f32>(1.0, 2.0, 1.0);
+    var p = vec3<f32>(fragCoord.x, fragCoord.y + 1.0, -1.0);
+    var dir = normalize(p - camera);
+
+    var i = 0;
+    loop {
+        if (i >= 90) { break; }
+        let dist = scene(p);
+        if (dist < 0.0001) { break; }
+        p = p + dir * dist;
+        i = i + 1;
+    }
+
+    let surf_normal = normal(p);
+    let light_position = vec3<f32>(2.0, 4.0, -0.5);
+    var light = 7.0 + 2.0 * dot(surf_normal, light_position);
+    light = light / (0.2 * pow(length(light_position - p), 3.5));
+
+    let alpha = select(0.0, 1.0, i < 90);
+    return vec4<f32>(light * light_color, alpha) * 2.0;
+}
+
+@fragment
+fn fs_main(@location(0) frag_position: vec2<f32>) -> @location(0) vec4<f32> {
+    let selected_light_color = pc.light_color_and_time.xyz;
+    let r = vec2<f32>(0.5 * frag_position.x + 1.0, 0.5 - 0.5 * frag_position.y);
+    return render(r, selected_light_color);
+}

+ 62 - 0
examples/wgpu-texture/src/styles.css

@@ -0,0 +1,62 @@
+* {
+    box-sizing: border-box;
+}
+
+html, body, main {
+    height: 100%;
+    font-family: system-ui, sans;
+    margin: 0;
+}
+
+main {
+    display: grid;
+    grid-template-rows: 100px 1fr;
+    grid-template-columns: 100%;
+    background: #f4e8d2;
+}
+
+#canvas-container {
+    display: grid;
+    opacity: 0.8;
+    grid-row: 2;
+}
+
+header {
+    padding: 10px 40px;
+    background-color: white;
+    z-index: 100;
+    grid-row: 1;
+}
+
+#overlay {
+    position: absolute;
+    width: 33%;
+    height: 100%;
+    right: 0;
+    z-index: 10;
+    background-color: rgba(0, 0, 0, 0.5);
+    padding-top: 40%;
+    padding-inline: 20px;
+    color: white;
+}
+
+#underlay {
+    position: absolute;
+    width: 33%;
+    height: 100%;
+    z-index: -10;
+    background-color: black;
+    padding-top: 40%;
+    padding-inline: 20px;
+    color: white;
+}
+
+.color-control {
+    display: flex;
+    gap: 12px;
+
+    > input {
+        width: 150px;
+        color: black;
+    }
+}

+ 2 - 1
lychee.toml

@@ -3,4 +3,5 @@ exclude = ['file:///', 'https://github.com/DioxusLabs/dioxus/commit']
 exclude_path = ['target']
 no_progress = false
 cache = true
-max_cache_age = "10d"
+max_cache_age = "10d"
+accept = ["200..=204", "429"]

二进制
notes/primitive-components.avif


+ 205 - 0
notes/releases/0.7.0-alpha.0.md

@@ -0,0 +1,205 @@
+## Hot-Patching, Native Renderer, Bundle Splitting, Radix-UI, more!
+
+> [!NOTE]
+> These release notes are a draft for the full release and thus are incomplete. Not all features might be merged yet!
+> We are releasing v0.7.0-alpha.0 with many docs and features incomplete, please be patient while we fill everything out.
+
+Welcome back to another Dioxus release! If you’re new here, Dioxus (dye • ox • us) is a framework for building cross-platform apps in Rust. We make it easy to ship fullstack web, desktop, and mobile apps with a single codebase.
+
+Dioxus 0.7 delivers on a number of promises we made to expand the capabilities of Rust GUI. Mature frameworks like Flutter and React Native sport capable hot-reload systems, popular component frameworks, and render natively. Now, Dioxus is on par with the “state of the art”, and in many ways, is even better.
+
+In this release, we’re shipping some incredible features. The highlights of this release include:
+
+- Dioxus Native: WGPU-based HTML/CSS Dioxus renderer built on Firefox’s Gecko engine
+- Subsecond: Hot-patching of Rust code at runtime
+- WASM-Split: Code splitting and tree shaking for WebAssembly
+- Dioxus-UI: Shadcn-UI implementation for Dioxus
+
+Dioxus 0.7 also brings a number of other exciting new features:
+
+- Automatic tailwind: zero-setup tailwind support built-in!
+- LLMs.txt: first-party context file to supercharge AI coding models
+- MCP Server: add context, resources, and tools to VSCode, Cursor, and Claude
+- Blitz: our modular HTML/CSS renderer powering Dioxus Native, available for everyone!
+- Dioxus Playground: online WASM/WASI playground with integrated hot-patching
+- Fullstack WebSockets: websockets in a single line of code
+- Integrated Debugger Support: open CodeLLDB or nvim DAP with a single keystroke
+- Fullstack status codes: Integration of status codes and custom errors in fullstack
+- Configurable Mobile Builds: Customize your AndroidManifest and Info.plist
+
+Plus, a number of quality-of-life upgrades:
+
+- one-line installer
+- `dx self-update` and update notifications
+- automatically open simulators
+- `dx` compatibility with non-dioxus projects
+- Better log coloring
+- desktop and mobile toasts
+- improved website landing page and migration to dioxus.dev
+- Reduced flicker on CSS hot-reloads
+- HTML streaming now waits for the router to render
+- Axum 0.8 upgrade
+
+And many, many bugs fixed:
+
+- Issues with synchronous multi-window
+- Tab focusing
+- Hot-reloaded assets not being re-processed
+
+## Note from the author
+
+Dioxus 0.7 marks the second anniversary of me (Jonathan Kelley) going full time on Dioxus. How time flies! In the past two years we shipped so much:
+
+- Template Hot-Reloading and Autoformatting
+- Migration to Signals
+- First-party Android and iOS tooling
+- Server Function integration
+- Linker-based asset system
+- and so much more!
+
+The road here has been long and frankly, lots of work. When we started out, the Rust ecosystem had very few good solutions to the basic problems in application development. Even now, the Rust hotpatching and native renderers - while incredible achievements on their own - are just “par for the course” for application development.
+
+With Dioxus 0.7, I feel like the Dioxus foundations are finally solid. We have excellent developer tools, lightning-fast hotpatching, a great asset system, a solid RPC solution, bundle splitting, automatic optimizations, autocomplete, autoformatting, a capable state management solution, comprehensive docs, and funding for the foreseeable future. It’s always nice to see that decisions to adopt industry-standard tech pay-off (Rust GUIs in 2025 article).
+
+What of the future? I finally feel like we’re on the “other side” of the once-impossible problems. With hot-patching and the native renderer behind us, we’re quite free to work on smaller projects. We could definitely use better marketing, more tutorial videos, better starter templates, and ecosystem growth (native APIs in 0.8!). Thanks for all the support so far!
+
+## Rust Hot-patching
+
+The biggest feature of this release: Dioxus now supports hot-patching of Rust code at runtime! You can now iterate on your app’s frontend and backend *simultaneously* without skipping a beat.
+
+
+We’ve been working on this feature for almost an *entire year,* so this is a very special release for us. The tool powering this hot-patching is called *Subsecond* and works across all major platforms: Web (WASM), Desktop (macOS, Linux, Windows), and even mobile (iOS, Android):
+
+
+Subsecond works in tandem with the Dioxus CLI to enable hot-patching for any Rust project. Simply run `dx serve` on your project and all `subsecond::call` sites will be hot-patched. For example, here’s Subsecond working with a Ratatui app:
+
+
+The infrastructure to support Subsecond is quite complex; consequently, we plan to only ship the Subsecond engine within the Dioxus CLI itself. However, we still want the ecosystem to experience the magic of Subsecond, so we’ve done two things:
+
+- Make `dx` a standalone runner, not tied to Dioxus
+- Integrated hotpatching with our new Dioxus Playground
+
+Hot-patching Rust code is no simple feat. To achieve a segfault-free experience, we recommend framework authors to tie into Subsecond’s minimal runtime. For application developers, you can simply use `subsecond::call(some_fn)` at clean integration points to take advantage of hot-patching. If you use Dioxus, hot-patching comes directly integrated with components and server functions.
+
+```rust
+pub fn launch() {
+    loop {
+        std::thread::sleep(std::time::Duration::from_secs(1));
+        subsecond::call(|| tick());
+    }
+}
+
+fn tick() {
+    println!("edit me to see the loop in action!!!!!!!!! ");
+}
+```
+
+While in theory we could *implicitly* override calls to `tick` with function detouring, we instead chose *explicit* integration points. Hot-patching encounters a significant challenge with changes to struct layout and alignment, and implicit patching exacerbates these safety issues. Explicit integration provides an opportunity frameworks to “re-instance” changed structs and guarantees a segfault-free experience at the cost of losing some runtime state.
+
+We expect folks to use Subsecond outside of Dioxus, namely in web development, so we’ve provided a few starter-integrations for popular libraries:
+
+- Axum
+- Bevy
+- Ratatui
+
+Hot-patching covers nearly *every* case in Dioxus - there’s so much you can hot-reload:
+
+
+
+Under the hood, we implemented a form of incremental linking / binary patching tailored for running apps. This is not too distant from the idea laid out by Andrew Kelley for Zig. We have yet to release an in-depth technical writeup about how Subsecond works, but if you’re really interested, come join us at the Seattle RustConf and learn about it during our talk!
+
+## Dioxus Native
+
+## WASM Bundle Splitting and Lazy Loading
+
+## Component Library: Radix Primitives and ShadCN-UI
+
+## LLMs.txt, Cursor Rules, MCP Server, and Vibe-Coding
+
+## Automatic Tailwind
+
+- `dx` now detects a `tailwind.css` file in the root of your crate
+- customize the input and output files in your dioxus.toml
+- Automatically downloads the tailwind binary in the background
+
+## Blitz 0.1
+
+We’re *extremely* excited to release Blitz: our modular HTML/CSS rendering engine.
+
+Blitz combines a number of exciting projects to bring customizable HTML rendering engine to everyone. Blitz is a result of collaboration across many projects: Firefox, Google, Servo, and Bevy. We’re leveraging a number of powerful libraries:
+
+- Taffy: our high-performance flexbox layout engine
+- Stylo: Firefox and Servo’s shared CSS resolution engine
+- Vello: Google’s GPU compute renderer
+
+Blitz is an extremely capable renderer, often producing results indistinguishable from browsers like Chrome and Safari:
+
+
+Not every CSS feature is supported yet, with some bugs like incorrect writing direction or the occasional layout quirk. Our support matrix is here: https://blitz-website.fly.dev/status/css
+
+The samples that Blitz can create are quite incredible. Servo’s website:
+
+
+Hackernews:
+
+
+The BBC:
+
+
+Do note that Blitz is still very young and doesn’t always produce the best outputs, especially on pages that require JavaScript to function properly or use less-popular CSS features:
+
+Blitz also provides a pluggable layer for interactivity, supporting actions like text inputs, pluggable widgets, form submissions, hover styling, and more. Here’s Dioxus-Motion working alongside our interactivity layer to provide high quality animations:
+
+
+Bear in mind that Blitz is still considered a “work in progress.” We have not focused on performance
+
+## Integrated Debugger
+
+To date, debugging Rust apps with VSCode hasn’t been particularly easy. Each combination of launch targets, flags, and arguments required a new entry into your `vscode.json` or `nvim.dap` file. With Dioxus 0.7, we wanted to improve debugging and now ship an integrated debugger. Simply press `d` and the current LLDB / DAP instance will attach to the app running under `dx serve`. By default, LLDB provides rather cryptic names and values while debugging, so we’ve built a prettifier that improves the rendering of Enums and Signals in the debugger.
+
+The integrated debugger works with VSCode and DAP setups and can be used to debug both the server and client simultaneously.
+
+## Improved Version Management Experience
+
+- one-line installer
+- `dx self-update` and update notifications
+
+## Dioxus Playground
+
+## Automatically open simulators
+
+## Desktop and Mobile toasts
+
+## Reduced flicker on CSS Hot-reloads
+
+## Better log coloring
+
+## Various Quality of Life Upgrades
+
+## Axum 0.8 Upgrade and Fullstack Improvements
+
+- HTML streaming now waits for the router to render
+- Axum 0.8 upgrade
+
+## Playground and Migration to `dioxus.dev`
+
+## Fullstack WebSockets, improved HTML streaming, and custom Error types
+
+## ADB Reverse Proxy for Device Hot-Reload
+
+## DX Compatibility with *any* project
+
+Dioxus’ `dx` tooling is now usable with any Rust project, not just Dioxus projects. You can use `dx` alongside many of the popular Rust projects.
+
+- Hot-Reload
+- Packaging/Bundling for Web/Desktop/Mobile
+- Assets / Mangnais
+
+Customize AndroidManifest.xml and Info.plist
+
+- pass in your own
+- or configure them via our top-level config for both
+
+## iPad Support
+
+## Hot-Dog Tutorial

+ 1 - 1
packages/asset-resolver/Cargo.toml

@@ -12,7 +12,7 @@ rust-version = "1.79.0"
 
 [dependencies]
 http = { workspace = true }
-urlencoding = { workspace = true }
+percent-encoding = { workspace = true }
 infer = { workspace = true }
 thiserror = { workspace = true }
 dioxus-cli-config = { workspace = true }

+ 13 - 3
packages/asset-resolver/src/lib.rs

@@ -14,11 +14,21 @@ pub enum AssetServeError {
     ResponseError(#[from] http::Error),
 }
 
-pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
+/// Serve an asset from the filesystem or a custom asset handler.
+///
+/// This method properly accesses the asset directory based on the platform and serves the asset
+/// wrapped in an HTTP response.
+///
+/// Platform specifics:
+/// - On the web, this returns AssetServerError since there's no filesystem access. Use `fetch` instead.
+/// - On Android, it attempts to load assets using the Android AssetManager.
+/// - On other platforms, it serves assets from the filesystem.
+pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
     // 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(path)
+        percent_encoding::percent_decode_str(path)
+            .decode_utf8()
             .expect("expected URL to be UTF-8 encoded")
             .as_ref(),
     );
@@ -72,7 +82,7 @@ pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetS
 /// - [ ] Linux (rpm)
 /// - [ ] Linux (deb)
 /// - [ ] Android
-#[allow(unused)]
+#[allow(unreachable_code)]
 fn get_asset_root() -> PathBuf {
     let cur_exe = std::env::current_exe().unwrap();
 

+ 2 - 2
packages/autofmt/tests/srcless/basic_expr.rsx

@@ -46,12 +46,12 @@ parse_quote! {
     }
     p {
         img {
-            src: asset!("/example-book/assets1/logo.png", ImageAssetOptions::new().with_avif()),
+            src: asset!("/example-book/assets1/logo.png", AssetOptions::image().with_avif()),
             alt: "some_local1",
             title: "",
         }
         img {
-            src: asset!("/example-book/assets2/logo.png", ImageAssetOptions::new().with_avif()),
+            src: asset!("/example-book/assets2/logo.png", AssetOptions::image().with_avif()),
             alt: "some_local2",
             title: "",
         }

+ 5 - 2
packages/cli-config/src/lib.rs

@@ -102,15 +102,18 @@ macro_rules! read_env_config {
 /// For reference, the devserver typically lives on `127.0.0.1:8080` and serves the devserver websocket
 /// on `127.0.0.1:8080/_dioxus`.
 pub fn devserver_raw_addr() -> Option<SocketAddr> {
-    let ip = std::env::var(DEVSERVER_IP_ENV).ok()?;
-    let port = std::env::var(DEVSERVER_PORT_ENV).ok()?;
+    let port = std::env::var(DEVSERVER_PORT_ENV).ok();
 
     if cfg!(target_os = "android") {
         // Since `adb reverse` is used for Android, the 127.0.0.1 will always be
         // the correct IP address.
+        let port = port.unwrap_or("8080".to_string());
         return Some(format!("127.0.0.1:{}", port).parse().unwrap());
     }
 
+    let port = port?;
+    let ip = std::env::var(DEVSERVER_IP_ENV).ok()?;
+
     format!("{}:{}", ip, port).parse().ok()
 }
 

+ 3 - 0
packages/cli-opt/Cargo.toml

@@ -67,3 +67,6 @@ swc_parallel = { version = "=1.0.1", default-features = false }
 swc_timer = { version = "=1.0.0", default-features = false }
 swc_visit = { version = "=2.0.0", default-features = false }
 browserslist-rs = { version = "=0.16.0" }
+
+[build-dependencies]
+built = { version = "0.7.5", features = ["git2"] }

+ 3 - 0
packages/cli-opt/build.rs

@@ -0,0 +1,3 @@
+fn main() {
+    built::write_built_file().expect("Failed to acquire build-time information");
+}

+ 10 - 0
packages/cli-opt/src/build_info.rs

@@ -0,0 +1,10 @@
+// The file has been placed there by the build script.
+include!(concat!(env!("OUT_DIR"), "/built.rs"));
+
+pub(crate) fn version() -> String {
+    format!(
+        "{} ({})",
+        PKG_VERSION,
+        GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
+    )
+}

+ 32 - 6
packages/cli-opt/src/css.rs

@@ -1,4 +1,4 @@
-use std::path::Path;
+use std::{hash::Hasher, path::Path};
 
 use anyhow::{anyhow, Context};
 use codemap::SpanLoc;
@@ -146,12 +146,11 @@ pub(crate) fn minify_css(css: &str) -> anyhow::Result<String> {
     Ok(res.code)
 }
 
-/// Process an scss/sass file into css.
-pub(crate) fn process_scss(
+/// Compile scss with grass
+pub(crate) fn compile_scss(
     scss_options: &CssAssetOptions,
     source: &Path,
-    output_path: &Path,
-) -> anyhow::Result<()> {
+) -> anyhow::Result<String> {
     let style = match scss_options.minified() {
         true => OutputStyle::Compressed,
         false => OutputStyle::Expanded,
@@ -162,7 +161,18 @@ pub(crate) fn process_scss(
         .quiet(false)
         .logger(&ScssLogger {});
 
-    let css = grass::from_path(source, &options)?;
+    let css = grass::from_path(source, &options)
+        .with_context(|| format!("Failed to compile scss file: {}", source.display()))?;
+    Ok(css)
+}
+
+/// Process an scss/sass file into css.
+pub(crate) fn process_scss(
+    scss_options: &CssAssetOptions,
+    source: &Path,
+    output_path: &Path,
+) -> anyhow::Result<()> {
+    let css = compile_scss(scss_options, source)?;
     let minified = minify_css(&css)?;
 
     std::fs::write(output_path, minified).with_context(|| {
@@ -199,3 +209,19 @@ impl grass::Logger for ScssLogger {
         );
     }
 }
+
+/// Hash the inputs to the scss file
+pub(crate) fn hash_scss(
+    scss_options: &CssAssetOptions,
+    source: &Path,
+    hasher: &mut impl Hasher,
+) -> anyhow::Result<()> {
+    // Grass doesn't expose the ast for us to traverse the imports in the file. Instead of parsing scss ourselves
+    // we just hash the expanded version of the file for now
+    let css = compile_scss(scss_options, source)?;
+
+    // Hash the compiled css
+    hasher.write(css.as_bytes());
+
+    Ok(())
+}

+ 90 - 48
packages/cli-opt/src/file.rs

@@ -1,5 +1,6 @@
 use anyhow::Context;
-use manganis_core::{AssetOptions, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
+use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
+use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
 use std::path::Path;
 
 use crate::css::{process_css_module, process_scss};
@@ -25,15 +26,15 @@ pub(crate) fn process_file_to_with_options(
     output_path: &Path,
     in_folder: bool,
 ) -> anyhow::Result<()> {
-    // If the file already exists, then we must have a file with the same hash
-    // already. The hash has the file contents and options, so if we find a file
-    // with the same hash, we probably already created this file in the past
-    if output_path.exists() {
+    // If the file already exists and this is a hashed asset, then we must have a file
+    // with the same hash already. The hash has the file contents and options, so if we
+    // find a file with the same hash, we probably already created this file in the past
+    if output_path.exists() && options.hash_suffix() {
         return Ok(());
     }
     if let Some(parent) = output_path.parent() {
         if !parent.exists() {
-            std::fs::create_dir_all(parent)?;
+            std::fs::create_dir_all(parent).context("Failed to create directory")?;
         }
     }
 
@@ -47,63 +48,104 @@ pub(crate) fn process_file_to_with_options(
             .unwrap_or_default()
             .to_string_lossy()
     ));
+    let resolved_options = resolve_asset_options(source, options.variant());
 
-    match options {
-        AssetOptions::Unknown => match source.extension().map(|e| e.to_string_lossy()).as_deref() {
-            Some("css") => {
-                process_css(&CssAssetOptions::new(), source, &temp_path)?;
-            }
-            Some("scss" | "sass") => {
-                process_scss(&CssAssetOptions::new(), source, &temp_path)?;
-            }
-            Some("js") => {
-                process_js(&JsAssetOptions::new(), source, &temp_path, !in_folder)?;
-            }
-            Some("json") => {
-                process_json(source, &temp_path)?;
-            }
-            Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
-                process_image(&ImageAssetOptions::new(), source, &temp_path)?;
-            }
-            Some(_) | None => {
-                if source.is_dir() {
-                    process_folder(source, &temp_path)?;
-                } else {
-                    let source_file = std::fs::File::open(source)?;
-                    let mut reader = std::io::BufReader::new(source_file);
-                    let output_file = std::fs::File::create(&temp_path)?;
-                    let mut writer = std::io::BufWriter::new(output_file);
-                    std::io::copy(&mut reader, &mut writer).with_context(|| {
-                        format!(
-                            "Failed to write file to output location: {}",
-                            temp_path.display()
-                        )
-                    })?;
-                }
-            }
-        },
-        AssetOptions::Css(options) => {
+    match &resolved_options {
+        ResolvedAssetType::Css(options) => {
             process_css(options, source, &temp_path)?;
         }
-        AssetOptions::CssModule(options) => {
+        ResolvedAssetType::CssModule(options) => {
             process_css_module(options, source, output_path, &temp_path)?;
         }
-        AssetOptions::Js(options) => {
+        ResolvedAssetType::Scss(options) => {
+            process_scss(options, source, &temp_path)?;
+        }
+        ResolvedAssetType::Js(options) => {
             process_js(options, source, &temp_path, !in_folder)?;
         }
-        AssetOptions::Image(options) => {
+        ResolvedAssetType::Image(options) => {
             process_image(options, source, &temp_path)?;
         }
-        AssetOptions::Folder(_) => {
+        ResolvedAssetType::Json => {
+            process_json(source, &temp_path)?;
+        }
+        ResolvedAssetType::Folder(_) => {
             process_folder(source, &temp_path)?;
         }
-        _ => {
-            tracing::warn!("Unknown asset options: {:?}", options);
+        ResolvedAssetType::File => {
+            let source_file = std::fs::File::open(source)?;
+            let mut reader = std::io::BufReader::new(source_file);
+            let output_file = std::fs::File::create(&temp_path)?;
+            let mut writer = std::io::BufWriter::new(output_file);
+            std::io::copy(&mut reader, &mut writer).with_context(|| {
+                format!(
+                    "Failed to write file to output location: {}",
+                    temp_path.display()
+                )
+            })?;
+        }
+    }
+
+    // Remove the existing output file if it exists
+    if output_path.exists() {
+        if output_path.is_file() {
+            std::fs::remove_file(output_path).context("Failed to remove previous output file")?;
+        } else if output_path.is_dir() {
+            std::fs::remove_dir_all(output_path)
+                .context("Failed to remove previous output file")?;
         }
     }
 
     // If everything was successful, rename the temp file to the final output path
-    std::fs::rename(temp_path, output_path)?;
+    std::fs::rename(temp_path, output_path).context("Failed to rename output file")?;
 
     Ok(())
 }
+
+pub(crate) enum ResolvedAssetType {
+    /// An image asset
+    Image(ImageAssetOptions),
+    /// A css asset
+    Css(CssAssetOptions),
+    /// A css module asset
+    CssModule(CssModuleAssetOptions),
+    /// A SCSS asset
+    Scss(CssAssetOptions),
+    /// A javascript asset
+    Js(JsAssetOptions),
+    /// A json asset
+    Json,
+    /// A folder asset
+    Folder(FolderAssetOptions),
+    /// A generic file
+    File,
+}
+
+pub(crate) fn resolve_asset_options(source: &Path, options: &AssetVariant) -> ResolvedAssetType {
+    match options {
+        AssetVariant::Image(image) => ResolvedAssetType::Image(*image),
+        AssetVariant::Css(css) => ResolvedAssetType::Css(*css),
+        AssetVariant::CssModule(css) => ResolvedAssetType::CssModule(*css),
+        AssetVariant::Js(js) => ResolvedAssetType::Js(*js),
+        AssetVariant::Folder(folder) => ResolvedAssetType::Folder(*folder),
+        AssetVariant::Unknown => resolve_unknown_asset_options(source),
+        _ => {
+            tracing::warn!("Unknown asset options... you may need to update the Dioxus CLI. Defaulting to a generic file: {:?}", options);
+            resolve_unknown_asset_options(source)
+        }
+    }
+}
+
+fn resolve_unknown_asset_options(source: &Path) -> ResolvedAssetType {
+    match source.extension().map(|e| e.to_string_lossy()).as_deref() {
+        Some("scss" | "sass") => ResolvedAssetType::Scss(CssAssetOptions::default()),
+        Some("css") => ResolvedAssetType::Css(CssAssetOptions::default()),
+        Some("js") => ResolvedAssetType::Js(JsAssetOptions::default()),
+        Some("json") => ResolvedAssetType::Json,
+        Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
+            ResolvedAssetType::Image(ImageAssetOptions::default())
+        }
+        _ if source.is_dir() => ResolvedAssetType::Folder(FolderAssetOptions::default()),
+        _ => ResolvedAssetType::File,
+    }
+}

+ 1 - 1
packages/cli-opt/src/folder.rs

@@ -33,7 +33,7 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
 /// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
 fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
     process_file_to_with_options(
-        &manganis_core::AssetOptions::Unknown,
+        &manganis_core::AssetOptions::builder().into_asset_options(),
         input_path,
         output_path,
         true,

+ 166 - 0
packages/cli-opt/src/hash.rs

@@ -0,0 +1,166 @@
+//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.
+
+use std::{
+    hash::{Hash, Hasher},
+    io::Read,
+    path::{Path, PathBuf},
+};
+
+use crate::{
+    css::hash_scss,
+    file::{resolve_asset_options, ResolvedAssetType},
+    js::hash_js,
+};
+use manganis::{AssetOptions, BundledAsset};
+
+/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
+/// change. This hash is included in the URL of the bundled asset for cache busting.
+struct AssetHash {
+    /// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
+    hash: [u8; 8],
+}
+
+impl AssetHash {
+    /// Create a new asset hash
+    const fn new(hash: u64) -> Self {
+        Self {
+            hash: hash.to_le_bytes(),
+        }
+    }
+
+    /// Get the hash bytes
+    pub const fn bytes(&self) -> &[u8] {
+        &self.hash
+    }
+
+    /// Create a new asset hash for a file. The input file to this function should be fully resolved
+    pub fn hash_file_contents(
+        options: &AssetOptions,
+        file_path: impl AsRef<Path>,
+    ) -> anyhow::Result<AssetHash> {
+        hash_file(options, file_path.as_ref())
+    }
+}
+
+/// Process a specific file asset with the given options reading from the source and writing to the output path
+fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash> {
+    // Create a hasher
+    let mut hash = std::collections::hash_map::DefaultHasher::new();
+    options.hash(&mut hash);
+    // Hash the version of CLI opt
+    hash.write(crate::build_info::version().as_bytes());
+    hash_file_with_options(options, source, &mut hash, false)?;
+
+    let hash = hash.finish();
+    Ok(AssetHash::new(hash))
+}
+
+/// Process a specific file asset with additional options
+pub(crate) fn hash_file_with_options(
+    options: &AssetOptions,
+    source: &Path,
+    hasher: &mut impl Hasher,
+    in_folder: bool,
+) -> anyhow::Result<()> {
+    let resolved_options = resolve_asset_options(source, options.variant());
+
+    match &resolved_options {
+        // Scss and JS can import files during the bundling process. We need to hash
+        // both the files themselves and any imports they have
+        ResolvedAssetType::Scss(options) => {
+            hash_scss(options, source, hasher)?;
+        }
+        ResolvedAssetType::Js(options) => {
+            hash_js(options, source, hasher, !in_folder)?;
+        }
+
+        // Otherwise, we can just hash the file contents
+        ResolvedAssetType::CssModule(_)
+        | ResolvedAssetType::Css(_)
+        | ResolvedAssetType::Image(_)
+        | ResolvedAssetType::Json
+        | ResolvedAssetType::File => {
+            hash_file_contents(source, hasher)?;
+        }
+        // Or the folder contents recursively
+        ResolvedAssetType::Folder(_) => {
+            let files = std::fs::read_dir(source)?;
+            for file in files.flatten() {
+                let path = file.path();
+                hash_file_with_options(options, &path, hasher, true)?;
+            }
+        }
+    }
+
+    Ok(())
+}
+
+pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> anyhow::Result<()> {
+    // Otherwise, open the file to get its contents
+    let mut file = std::fs::File::open(source)?;
+
+    // We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
+    // The hash includes the file contents, the options, and the version of manganis. From the macro, we just
+    // know the file contents, so we only include that hash
+    let mut buffer = [0; 8192];
+    loop {
+        let read = file.read(&mut buffer)?;
+        if read == 0 {
+            break;
+        }
+        hasher.write(&buffer[..read]);
+    }
+    Ok(())
+}
+
+/// Add a hash to the asset, or log an error if it fails
+pub fn add_hash_to_asset(asset: &mut BundledAsset) {
+    let source = asset.absolute_source_path();
+    match AssetHash::hash_file_contents(asset.options(), source) {
+        Ok(hash) => {
+            let options = *asset.options();
+
+            // Set the bundled path to the source path with the hash appended before the extension
+            let source_path = PathBuf::from(source);
+            let Some(file_name) = source_path.file_name() else {
+                tracing::error!("Failed to get file name from path: {source}");
+                return;
+            };
+            // The output extension path is the extension set by the options
+            // or the extension of the source file if we don't recognize the file
+            let mut ext = asset.options().extension().map(Into::into).or_else(|| {
+                source_path
+                    .extension()
+                    .map(|ext| ext.to_string_lossy().to_string())
+            });
+
+            // Rewrite scss as css
+            if let Some("scss" | "sass") = ext.as_deref() {
+                ext = Some("css".to_string());
+            }
+
+            let hash = hash.bytes();
+            let hash = hash
+                .iter()
+                .map(|byte| format!("{byte:x}"))
+                .collect::<String>();
+            let file_stem = source_path.file_stem().unwrap_or(file_name);
+            let mut bundled_path = if asset.options().hash_suffix() {
+                PathBuf::from(format!("{}-dxh{hash}", file_stem.to_string_lossy()))
+            } else {
+                PathBuf::from(file_stem)
+            };
+
+            if let Some(ext) = ext {
+                bundled_path.set_extension(ext);
+            }
+
+            let bundled_path = bundled_path.to_string_lossy().to_string();
+
+            *asset = BundledAsset::new(source, &bundled_path, options);
+        }
+        Err(err) => {
+            tracing::error!("Failed to hash asset: {err}");
+        }
+    }
+}

+ 23 - 12
packages/cli-opt/src/image/mod.rs

@@ -14,7 +14,8 @@ pub(crate) fn process_image(
     output_path: &Path,
 ) -> anyhow::Result<()> {
     let mut image = image::ImageReader::new(std::io::Cursor::new(&*std::fs::read(source)?))
-        .with_guessed_format()?
+        .with_guessed_format()
+        .context("Failed to guess image format")?
         .decode();
 
     if let Ok(image) = &mut image {
@@ -25,10 +26,10 @@ pub(crate) fn process_image(
 
     match (image, image_options.format()) {
         (image, ImageFormat::Png) => {
-            compress_png(image?, output_path);
+            compress_png(image.context("Failed to decode image")?, output_path);
         }
         (image, ImageFormat::Jpg) => {
-            compress_jpg(image?, output_path)?;
+            compress_jpg(image.context("Failed to decode image")?, output_path)?;
         }
         (Ok(image), ImageFormat::Avif) => {
             if let Err(error) = image.save(output_path) {
@@ -41,20 +42,30 @@ pub(crate) fn process_image(
             }
         }
         (Ok(image), _) => {
-            image.save(output_path)?;
+            image.save(output_path).with_context(|| {
+                format!(
+                    "Failed to save image (from {}) with path {}",
+                    source.display(),
+                    output_path.display()
+                )
+            })?;
         }
         // If we can't decode the image or it is of an unknown type, we just copy the file
         _ => {
-            let source_file = std::fs::File::open(source)?;
+            let source_file = std::fs::File::open(source).context("Failed to open source file")?;
             let mut reader = std::io::BufReader::new(source_file);
-            let output_file = std::fs::File::create(output_path)?;
-            let mut writer = std::io::BufWriter::new(output_file);
-            std::io::copy(&mut reader, &mut writer).with_context(|| {
-                format!(
-                    "Failed to write image to output location: {}",
-                    output_path.display()
-                )
+            let output_file = std::fs::File::create(output_path).with_context(|| {
+                format!("Failed to create output file: {}", output_path.display())
             })?;
+            let mut writer = std::io::BufWriter::new(output_file);
+            std::io::copy(&mut reader, &mut writer)
+                .with_context(|| {
+                    format!(
+                        "Failed to write image to output location: {}",
+                        output_path.display()
+                    )
+                })
+                .context("Failed to copy image data")?;
         }
     }
 

+ 62 - 15
packages/cli-opt/src/js.rs

@@ -1,3 +1,4 @@
+use std::hash::Hasher;
 use std::path::Path;
 use std::path::PathBuf;
 
@@ -24,6 +25,8 @@ use swc_ecma_codegen::text_writer::JsWriter;
 use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
 use swc_ecma_parser::{parse_file_as_module, Syntax};
 
+use crate::hash::hash_file_contents;
+
 struct TracingEmitter;
 
 impl Emitter for TracingEmitter {
@@ -43,30 +46,32 @@ impl Emitter for TracingEmitter {
     }
 }
 
+/// Run a closure with the swc globals and handler set up
+fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
+    let globals = Globals::new();
+    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
+    let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
+    GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
+}
+
 fn bundle_js_to_writer(
     file: PathBuf,
     bundle: bool,
     minify: bool,
     write_to: &mut impl std::io::Write,
 ) -> anyhow::Result<()> {
-    let globals = Globals::new();
-    let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
-    GLOBALS.set(&globals, || {
-        HANDLER.set(&handler, || {
-            bundle_js_to_writer_inside_handler(&globals, file, bundle, minify, write_to)
-        })
+    inside_handler(|globals, cm| {
+        bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
     })
 }
 
-fn bundle_js_to_writer_inside_handler(
+fn resolve_js_inside_handler(
     globals: &Globals,
     file: PathBuf,
     bundle: bool,
-    minify: bool,
-    write_to: &mut impl std::io::Write,
-) -> anyhow::Result<()> {
-    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
-    let mut module = if bundle {
+    cm: &Lrc<SourceMap>,
+) -> anyhow::Result<Module> {
+    if bundle {
         let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
         let mut bundler = Bundler::new(
             globals,
@@ -89,7 +94,7 @@ fn bundle_js_to_writer_inside_handler(
         let bundle = bundles
             .pop()
             .ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
-        bundle.module
+        Ok(bundle.module)
     } else {
         let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
 
@@ -108,8 +113,19 @@ fn bundle_js_to_writer_inside_handler(
                 error.cancel();
                 anyhow::anyhow!("{}", error.message())
             })
-        })?
-    };
+        })
+    }
+}
+
+fn bundle_js_to_writer_inside_handler(
+    globals: &Globals,
+    cm: Lrc<SourceMap>,
+    file: PathBuf,
+    bundle: bool,
+    minify: bool,
+    write_to: &mut impl std::io::Write,
+) -> anyhow::Result<()> {
+    let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;
 
     if minify {
         module = swc_ecma_minifier::optimize(
@@ -246,3 +262,34 @@ pub(crate) fn process_js(
 
     Ok(())
 }
+
+fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
+    inside_handler(|globals, cm| {
+        _ = resolve_js_inside_handler(globals, file, bundle, &cm)?;
+
+        for file in cm.files().iter() {
+            let hash = file.src_hash;
+            hasher.write(&hash.to_le_bytes());
+        }
+
+        Ok(())
+    })
+}
+
+pub(crate) fn hash_js(
+    js_options: &JsAssetOptions,
+    source: &Path,
+    hasher: &mut impl Hasher,
+    bundle: bool,
+) -> anyhow::Result<()> {
+    if js_options.minified() {
+        if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
+            tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
+            hash_file_contents(source, hasher)?;
+        }
+    } else {
+        hash_file_contents(source, hasher)?;
+    }
+
+    Ok(())
+}

+ 69 - 76
packages/cli-opt/src/lib.rs

@@ -1,19 +1,23 @@
 use anyhow::Context;
-use manganis_core::linker::LinkSection;
+use manganis::AssetOptions;
 use manganis_core::BundledAsset;
-use object::{read::archive::ArchiveFile, File as ObjectFile, Object, ObjectSection};
+use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
 use serde::{Deserialize, Serialize};
-use std::path::Path;
-use std::{collections::HashMap, path::PathBuf};
+use std::collections::{HashMap, HashSet};
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, RwLock};
 
+mod build_info;
 mod css;
 mod file;
 mod folder;
+mod hash;
 mod image;
 mod js;
 mod json;
 
 pub use file::process_file_to;
+pub use hash::add_hash_to_asset;
 
 /// A manifest of all assets collected from dependencies
 ///
@@ -21,7 +25,7 @@ pub use file::process_file_to;
 #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
 pub struct AssetManifest {
     /// Map of bundled asset name to the asset itself
-    pub assets: HashMap<PathBuf, BundledAsset>,
+    assets: HashMap<PathBuf, HashSet<BundledAsset>>,
 }
 
 impl AssetManifest {
@@ -31,100 +35,89 @@ impl AssetManifest {
         asset_path: &Path,
         options: manganis::AssetOptions,
     ) -> anyhow::Result<BundledAsset> {
-        let hash = manganis_core::hash::AssetHash::hash_file_contents(asset_path)
-            .context("Failed to hash file")?;
-
         let output_path_str = asset_path.to_str().ok_or(anyhow::anyhow!(
             "Failed to convert wasm bindgen output path to string"
         ))?;
 
-        let bundled_asset =
-            manganis::macro_helpers::create_bundled_asset(output_path_str, hash.bytes(), options);
+        let mut bundled_asset =
+            manganis::macro_helpers::create_bundled_asset(output_path_str, options);
+        add_hash_to_asset(&mut bundled_asset);
 
-        self.assets.insert(asset_path.into(), bundled_asset);
+        self.assets
+            .entry(asset_path.to_path_buf())
+            .or_default()
+            .insert(bundled_asset);
 
         Ok(bundled_asset)
     }
 
-    #[allow(dead_code)]
-    pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
-        let src = std::fs::read_to_string(path)?;
-
-        serde_json::from_str(&src)
-            .with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
+    /// Insert an existing bundled asset to the manifest
+    pub fn insert_asset(&mut self, asset: BundledAsset) {
+        let asset_path = asset.absolute_source_path();
+        self.assets
+            .entry(asset_path.into())
+            .or_default()
+            .insert(asset);
     }
 
-    /// Fill this manifest with a file object/rlib files, typically extracted from the linker intercepted
-    pub fn add_from_object_path(&mut self, path: &Path) -> anyhow::Result<()> {
-        let data = std::fs::read(path)?;
-
-        match path.extension().and_then(|ext| ext.to_str()) {
-            // Parse an rlib as a collection of objects
-            Some("rlib") => {
-                if let Ok(archive) = object::read::archive::ArchiveFile::parse(&*data) {
-                    self.add_from_archive_file(&archive, &data)?;
-                }
-            }
-            _ => {
-                if let Ok(object) = object::File::parse(&*data) {
-                    self.add_from_object_file(&object)?;
-                }
-            }
-        }
+    /// Get any assets that are tied to a specific source file
+    pub fn get_assets_for_source(&self, path: &Path) -> Option<&HashSet<BundledAsset>> {
+        self.assets.get(path)
+    }
 
-        Ok(())
+    /// Get the first asset that matches the given source path
+    pub fn get_first_asset_for_source(&self, path: &Path) -> Option<&BundledAsset> {
+        self.assets
+            .get(path)
+            .and_then(|assets| assets.iter().next())
     }
 
-    /// Fill this manifest from an rlib / ar file that contains many object files and their entries
-    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);
-            }
-        }
+    /// Check if the manifest contains a specific asset
+    pub fn contains(&self, asset: &BundledAsset) -> bool {
+        self.assets
+            .get(&PathBuf::from(asset.absolute_source_path()))
+            .is_some_and(|assets| assets.contains(asset))
+    }
 
-        Ok(())
+    /// Iterate over all the assets in the manifest
+    pub fn assets(&self) -> impl Iterator<Item = &BundledAsset> {
+        self.assets.values().flat_map(|assets| assets.iter())
     }
 
-    /// 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;
-            };
+    pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
+        let src = std::fs::read_to_string(path)?;
 
-            // 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);
+        serde_json::from_str(&src)
+            .with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
+    }
+}
 
-            if !matches {
-                continue;
+/// Optimize a list of assets in parallel
+pub fn optimize_all_assets(
+    assets_to_transfer: Vec<(PathBuf, PathBuf, AssetOptions)>,
+    on_optimization_start: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
+    on_optimization_end: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
+) -> anyhow::Result<()> {
+    let on_optimization_start = Arc::new(RwLock::new(on_optimization_start));
+    let on_optimization_end = Arc::new(RwLock::new(on_optimization_end));
+    assets_to_transfer
+        .par_iter()
+        .try_for_each(|(from, to, options)| {
+            {
+                let mut on_optimization_start = on_optimization_start.write().unwrap();
+                on_optimization_start(from, to, options);
             }
 
-            let bytes = section
-                .uncompressed_data()
-                .context("Could not read uncompressed data from object file")?;
+            let res = process_file_to(options, from, to);
+            if let Err(err) = res.as_ref() {
+                tracing::error!("Failed to copy asset {from:?}: {err}");
+            }
 
-            let mut buffer = const_serialize::ConstReadBuffer::new(&bytes);
-            while let Some((remaining_buffer, asset)) =
-                const_serialize::deserialize_const!(BundledAsset, buffer)
             {
-                self.assets
-                    .insert(asset.absolute_source_path().into(), asset);
-                buffer = remaining_buffer;
+                let mut on_optimization_end = on_optimization_end.write().unwrap();
+                on_optimization_end(from, to, options);
             }
-        }
 
-        Ok(())
-    }
+            res.map(|_| ())
+        })
 }

+ 13 - 16
packages/cli/Cargo.toml

@@ -50,7 +50,6 @@ hyper-rustls = { workspace = true }
 rustls = { workspace = true }
 rayon = { workspace = true }
 futures-channel = { workspace = true }
-target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
 krates = { workspace = true }
 regex = "1.11.1"
 console = "0.15.11"
@@ -61,7 +60,12 @@ axum-server = { workspace = true, features = ["tls-rustls"] }
 axum-extra = { workspace = true, features = ["typed-header"] }
 tower-http = { workspace = true, features = ["full"] }
 proc-macro2 = { workspace = true, features = ["span-locations"] }
-syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] }
+syn = { workspace = true, features = [
+    "full",
+    "extra-traits",
+    "visit",
+    "visit-mut",
+] }
 
 headers = "0.4.0"
 walkdir = "2"
@@ -71,7 +75,6 @@ dunce = { workspace = true }
 dirs = { workspace = true }
 reqwest = { workspace = true, features = ["rustls-tls", "trust-dns", "json"] }
 tower = { workspace = true }
-once_cell = { workspace = true }
 
 # path lookup
 which = { version = "7.0.2" }
@@ -109,6 +112,8 @@ log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] }
 tempfile = "3.13"
 manganis = { workspace = true }
 manganis-core = { workspace = true }
+target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
+wasm-encoder = "0.229.0"
 
 # Extracting data from an executable
 object = { workspace = true, features = ["all"] }
@@ -131,11 +136,13 @@ local-ip-address = "0.6.3"
 dircpy = "0.3.19"
 plist = "1.7.0"
 memoize = "0.5.1"
-wasm-encoder = "0.228.0"
 backtrace = "0.3.74"
 ar = "0.9.0"
 wasm-bindgen-externref-xform = "0.2.100"
 pdb = "0.8.0"
+self_update = { version = "0.42.0", features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate"] }
+self-replace = "1.5.0"
+cargo-config2 = { workspace = true }
 
 [build-dependencies]
 built = { version = "0.7.5", features = ["git2"] }
@@ -143,17 +150,10 @@ built = { version = "0.7.5", features = ["git2"] }
 [features]
 default = []
 plugin = []
-tokio-console = ["dep:console-subscriber"]
+tokio-console = ["dep:console-subscriber", "tokio/tracing"]
 bundle = []
 no-downloads = []
 
-# 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"
@@ -162,10 +162,7 @@ name = "dx"
 escargot = "0.5"
 
 [package.metadata.binstall]
-pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }-v{ version }{ archive-suffix }"
-pkg-fmt = "tgz"
-
-[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
+pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
 pkg-fmt = "zip"
 
 [package.metadata.docs.rs]

+ 0 - 2
packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
     <uses-permission android:name="android.permission.INTERNET" />
     <application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
         android:extractNativeLibs="true"
@@ -16,5 +15,4 @@
             </intent-filter>
         </activity>
     </application>
-
 </manifest>

+ 1 - 1
packages/cli/assets/web/dev.index.html

@@ -7,7 +7,7 @@
         <meta charset="UTF-8">
         <style>
             /* Inter Font */
-            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
+            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap') layer;
 
             #dx-toast-template {
                 display: none;

+ 1 - 0
packages/cli/assets/web/prod.index.html

@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
     <head>
         <title>{app_title}</title>

+ 370 - 0
packages/cli/src/build/assets.rs

@@ -0,0 +1,370 @@
+//! The dioxus asset system.
+//!
+//! This module provides functionality for extracting assets from a binary file and then writing back
+//! their asset hashes directly into the binary file. Previously, we performed asset hashing in the
+//! `asset!()` macro. The new system, implemented here, instead performs the hashing at build time,
+//! which provides more flexibility in the asset processing pipeline.
+//!
+//! We chose to implement this approach since assets might reference each other which means we minimally
+//! need to parse the asset to create a unique hash for each asset before they are used in the application.
+//! The hashes are used both for cache busting the asset in the browser and to cache the asset optimization
+//! process in the build system.
+//!
+//! We use the same lessons learned from the hot-patching engine which parses the binary file and its
+//! symbol table to find symbols that match the `__MANGANIS__` prefix. These symbols are ideally data
+//! symbols and contain the BundledAsset data type which implements ConstSerialize and ConstDeserialize.
+//!
+//! When the binary is built, the `dioxus asset!()` macro will emit its metadata into the __MANGANIS__
+//! symbols, which we process here. After reading the metadata directly from the executable, we then
+//! hash it and write the hash directly into the binary file.
+//!
+//! During development, we can skip this step for most platforms since local paths are sufficient
+//! for asset loading. However, for WASM and for production builds, we need to ensure that assets
+//! can be found relative to the current exe. Unfortunately, on android, the `current_exe` path is wrong,
+//! so the assets are resolved against the "asset root" - which is covered by the asset loader crate.
+//!
+//! Finding the __MANGANIS__ symbols is not quite straightforward when hotpatching, especially on WASM
+//! since we build and link the module as relocatable, which is not a stable WASM proposal. In this
+//! implementation, we handle both the non-PIE *and* PIC cases which are rather bespoke to our whole
+//! build system.
+
+use std::{
+    io::{Cursor, Read, Seek, Write},
+    path::{Path, PathBuf},
+};
+
+use crate::Result;
+use anyhow::Context;
+use const_serialize::{ConstVec, SerializeConst};
+use dioxus_cli_opt::AssetManifest;
+use manganis::BundledAsset;
+use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
+use pdb::FallibleIterator;
+use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
+
+/// Extract all manganis symbols and their sections from the given object file.
+fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
+    file: &'b File<'a, R>,
+) -> impl Iterator<Item = (Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
+    file.symbols()
+        .filter(|symbol| {
+            if let Ok(name) = symbol.name() {
+                looks_like_manganis_symbol(name)
+            } else {
+                false
+            }
+        })
+        .filter_map(move |symbol| {
+            let section_index = symbol.section_index()?;
+            let section = file.section_by_index(section_index).ok()?;
+            Some((symbol, section))
+        })
+}
+
+fn looks_like_manganis_symbol(name: &str) -> bool {
+    name.contains("__MANGANIS__")
+}
+
+/// Find the offsets of any manganis symbols in the given file.
+fn find_symbol_offsets<'a, R: ReadRef<'a>>(
+    path: &Path,
+    file_contents: &[u8],
+    file: &File<'a, R>,
+) -> Result<Vec<u64>> {
+    let pdb_file = find_pdb_file(path);
+
+    match file.format() {
+        // We need to handle dynamic offsets in wasm files differently
+        object::BinaryFormat::Wasm => find_wasm_symbol_offsets(file_contents, file),
+        // Windows puts the symbol information in a PDB file alongside the executable.
+        // If this is a windows PE file and we found a PDB file, we will use that to find the symbol offsets.
+        object::BinaryFormat::Pe if pdb_file.is_some() => {
+            find_pdb_symbol_offsets(&pdb_file.unwrap())
+        }
+        // Otherwise, look for manganis symbols in the object file.
+        _ => find_native_symbol_offsets(file),
+    }
+}
+
+/// Find the pdb file matching the executable file.
+fn find_pdb_file(path: &Path) -> Option<PathBuf> {
+    let mut pdb_file = path.with_extension("pdb");
+    // Also try to find it in the same directory as the executable with _'s instead of -'s
+    if let Some(file_name) = pdb_file.file_name() {
+        let new_file_name = file_name.to_string_lossy().replace('-', "_");
+        let altrnate_pdb_file = pdb_file.with_file_name(new_file_name);
+        // Keep the most recent pdb file
+        match (pdb_file.metadata(), altrnate_pdb_file.metadata()) {
+            (Ok(pdb_metadata), Ok(alternate_metadata)) => {
+                if let (Ok(pdb_modified), Ok(alternate_modified)) =
+                    (pdb_metadata.modified(), alternate_metadata.modified())
+                {
+                    if pdb_modified < alternate_modified {
+                        pdb_file = altrnate_pdb_file;
+                    }
+                }
+            }
+            (Err(_), Ok(_)) => {
+                pdb_file = altrnate_pdb_file;
+            }
+            _ => {}
+        }
+    }
+    if pdb_file.exists() {
+        Some(pdb_file)
+    } else {
+        None
+    }
+}
+
+/// Find the offsets of any manganis symbols in a pdb file.
+fn find_pdb_symbol_offsets(pdb_file: &Path) -> Result<Vec<u64>> {
+    let pdb_file_handle = std::fs::File::open(pdb_file)?;
+    let mut pdb_file = pdb::PDB::open(pdb_file_handle).context("Failed to open PDB file")?;
+    let Ok(Some(sections)) = pdb_file.sections() else {
+        tracing::error!("Failed to read sections from PDB file");
+        return Ok(Vec::new());
+    };
+    let global_symbols = pdb_file
+        .global_symbols()
+        .context("Failed to read global symbols from PDB file")?;
+    let address_map = pdb_file
+        .address_map()
+        .context("Failed to read address map from PDB file")?;
+    let mut symbols = global_symbols.iter();
+    let mut addresses = Vec::new();
+    while let Ok(Some(symbol)) = symbols.next() {
+        let Ok(pdb::SymbolData::Public(data)) = symbol.parse() else {
+            continue;
+        };
+        let Some(rva) = data.offset.to_section_offset(&address_map) else {
+            continue;
+        };
+
+        let name = data.name.to_string();
+        if name.contains("__MANGANIS__") {
+            let section = sections
+                .get(rva.section as usize - 1)
+                .expect("Section index out of bounds");
+
+            addresses.push((section.pointer_to_raw_data + rva.offset) as u64);
+        }
+    }
+    Ok(addresses)
+}
+
+/// Find the offsets of any manganis symbols in a native object file.
+fn find_native_symbol_offsets<'a, R: ReadRef<'a>>(file: &File<'a, R>) -> Result<Vec<u64>> {
+    let mut offsets = Vec::new();
+    for (symbol, section) in manganis_symbols(file) {
+        let virtual_address = symbol.address();
+
+        let Some((section_range_start, _)) = section.file_range() else {
+            tracing::error!(
+                "Found __MANGANIS__ symbol {:?} in section {}, but the section has no file range",
+                symbol.name(),
+                section.index()
+            );
+            continue;
+        };
+        // Translate the section_relative_address to the file offset
+        let section_relative_address: u64 = (virtual_address as i128 - section.address() as i128)
+            .try_into()
+            .expect("Virtual address should be greater than or equal to section address");
+        let file_offset = section_range_start + section_relative_address;
+        offsets.push(file_offset);
+    }
+
+    Ok(offsets)
+}
+
+fn eval_walrus_global_expr(module: &walrus::Module, expr: &walrus::ConstExpr) -> Option<u64> {
+    match expr {
+        walrus::ConstExpr::Value(walrus::ir::Value::I32(value)) => Some(*value as u64),
+        walrus::ConstExpr::Value(walrus::ir::Value::I64(value)) => Some(*value as u64),
+        walrus::ConstExpr::Global(id) => {
+            let global = module.globals.get(*id);
+            if let walrus::GlobalKind::Local(pointer) = &global.kind {
+                eval_walrus_global_expr(module, pointer)
+            } else {
+                None
+            }
+        }
+        _ => None,
+    }
+}
+
+/// Find the offsets of any manganis symbols in the wasm file.
+fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
+    file_contents: &[u8],
+    file: &File<'a, R>,
+) -> Result<Vec<u64>> {
+    let Some(section) = file
+        .sections()
+        .find(|section| section.name() == Ok("<data>"))
+    else {
+        tracing::error!("Failed to find <data> section in WASM file");
+        return Ok(Vec::new());
+    };
+    let Some((_, section_range_end)) = section.file_range() else {
+        tracing::error!("Failed to find file range for <data> section in WASM file");
+        return Ok(Vec::new());
+    };
+    let section_size = section.data()?.len() as u64;
+    let section_start = section_range_end - section_size;
+
+    // Translate the section_relative_address to the file offset
+    // WASM files have a section address of 0 in object, reparse the data section with wasmparser
+    // to get the correct address and section start
+    // Note: We need to reparse just the data section with wasmparser to get the file offset because walrus does
+    // not expose the file offset information
+    let reader = wasmparser::DataSectionReader::new(wasmparser::BinaryReader::new(
+        &file_contents[section_start as usize..section_range_end as usize],
+        0,
+    ))
+    .context("Failed to create WASM data section reader")?;
+    let main_memory = reader
+        .into_iter()
+        .next()
+        .context("Failed find main memory from WASM data section")?
+        .context("Failed to read main memory from WASM data section")?;
+    // main_memory.data is a slice somewhere in file_contents. Find out the offset in the file
+    let data_start_offset = (main_memory.data.as_ptr() as u64)
+        .checked_sub(file_contents.as_ptr() as u64)
+        .expect("Data section start offset should be within the file contents");
+
+    // Parse the wasm file to find the globals
+    let module = walrus::Module::from_buffer(file_contents).unwrap();
+    let mut offsets = Vec::new();
+
+    // Find the main memory offset
+    let main_memory = module
+        .data
+        .iter()
+        .next()
+        .context("Failed to find main memory in WASM module")?;
+
+    let walrus::DataKind::Active {
+        offset: main_memory_offset,
+        ..
+    } = main_memory.kind
+    else {
+        tracing::error!("Failed to find main memory offset in WASM module");
+        return Ok(Vec::new());
+    };
+
+    // In the hot patch build, the main memory offset is a global from the main module and each global
+    // is it's own global. Use an offset of 0 instead if we can't evaluate the global
+    let main_memory_offset =
+        eval_walrus_global_expr(&module, &main_memory_offset).unwrap_or_default();
+
+    for export in module.exports.iter() {
+        if !looks_like_manganis_symbol(&export.name) {
+            continue;
+        }
+
+        let walrus::ExportItem::Global(global) = export.item else {
+            continue;
+        };
+
+        let walrus::GlobalKind::Local(pointer) = module.globals.get(global).kind else {
+            continue;
+        };
+
+        let Some(virtual_address) = eval_walrus_global_expr(&module, &pointer) else {
+            tracing::error!(
+                "Found __MANGANIS__ symbol {:?} in WASM file, but the global expression could not be evaluated",
+                export.name
+            );
+            continue;
+        };
+
+        let section_relative_address: u64 = ((virtual_address as i128)
+            - main_memory_offset as i128)
+            .try_into()
+            .expect("Virtual address should be greater than or equal to section address");
+        let file_offset = data_start_offset + section_relative_address;
+
+        offsets.push(file_offset);
+    }
+
+    Ok(offsets)
+}
+
+/// Find all assets in the given file, hash them, and write them back to the file.
+/// Then return an `AssetManifest` containing all the assets found in the file.
+pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetManifest> {
+    let path = path.as_ref();
+    let mut file = std::fs::File::options().write(true).read(true).open(path)?;
+    let mut file_contents = Vec::new();
+    file.read_to_end(&mut file_contents)?;
+    let mut reader = Cursor::new(&file_contents);
+    let read_cache = ReadCache::new(&mut reader);
+    let object_file = object::File::parse(&read_cache)?;
+    let offsets = find_symbol_offsets(path, &file_contents, &object_file)?;
+
+    let mut assets = Vec::new();
+
+    // Read each asset from the data section using the offsets
+    for offset in offsets.iter().copied() {
+        file.seek(std::io::SeekFrom::Start(offset))?;
+        let mut data_in_range = vec![0; BundledAsset::MEMORY_LAYOUT.size()];
+        file.read_exact(&mut data_in_range)?;
+
+        let buffer = const_serialize::ConstReadBuffer::new(&data_in_range);
+
+        if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
+        {
+            tracing::debug!("Found asset at offset {offset}: {:?}", bundled_asset);
+            assets.push(bundled_asset);
+        } else {
+            tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
+        }
+    }
+
+    // Add the hash to each asset in parallel
+    assets
+        .par_iter_mut()
+        .for_each(dioxus_cli_opt::add_hash_to_asset);
+
+    // Write back the assets to the binary file
+    for (offset, asset) in offsets.into_iter().zip(&assets) {
+        tracing::debug!("Writing asset to offset {offset}: {:?}", asset);
+        let new_data = ConstVec::new();
+        let new_data = const_serialize::serialize_const(asset, new_data);
+
+        file.seek(std::io::SeekFrom::Start(offset))?;
+        // Write the modified binary data back to the file
+        file.write_all(new_data.as_ref())?;
+    }
+    // Ensure the file is flushed to disk
+    file.sync_all()
+        .context("Failed to sync file after writing assets")?;
+
+    // If the file is a macos binary, we need to re-sign the modified binary
+    if object_file.format() == object::BinaryFormat::MachO {
+        // Spawn the codesign command to re-sign the binary
+        let output = std::process::Command::new("codesign")
+            .arg("--force")
+            .arg("--sign")
+            .arg("-") // Sign with an empty identity
+            .arg(path)
+            .output()?;
+
+        if !output.status.success() {
+            return Err(anyhow::anyhow!(
+                "Failed to re-sign the binary with codesign after finalizing the assets: {}",
+                String::from_utf8_lossy(&output.stderr)
+            )
+            .into());
+        }
+    }
+
+    // Finally, create the asset manifest
+    let mut manifest = AssetManifest::default();
+    for asset in assets {
+        manifest.insert_asset(asset);
+    }
+
+    Ok(manifest)
+}

+ 402 - 169
packages/cli/src/build/builder.rs

@@ -1,10 +1,11 @@
 use crate::{
-    BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform, ProgressRx, ProgressTx,
-    Result, StructuredOutput,
+    serve::WebServer, BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform,
+    ProgressRx, ProgressTx, Result, StructuredOutput,
 };
 use anyhow::Context;
 use dioxus_cli_opt::process_file_to;
-use futures_util::future::OptionFuture;
+use futures_util::{future::OptionFuture, pin_mut, FutureExt};
+use itertools::Itertools;
 use std::{
     env,
     time::{Duration, Instant, SystemTime},
@@ -91,6 +92,9 @@ pub(crate) struct AppBuilder {
     pub compile_end: Option<Instant>,
     pub bundle_start: Option<Instant>,
     pub bundle_end: Option<Instant>,
+
+    /// The debugger for the app - must be enabled with the `d` key
+    pub(crate) pid: Option<u32>,
 }
 
 impl AppBuilder {
@@ -156,6 +160,7 @@ impl AppBuilder {
             entropy_app_exe: None,
             artifacts: None,
             patch_cache: None,
+            pid: None,
         })
     }
 
@@ -183,12 +188,16 @@ impl AppBuilder {
                 StderrReceived {  msg }
             },
             Some(status) = OptionFuture::from(self.child.as_mut().map(|f| f.wait())) => {
-                // Panicking here is on purpose. If the task crashes due to a JoinError (a panic),
-                // we want to propagate that panic up to the serve controller.
-                let status = status.unwrap();
-                self.child = None;
-
-                ProcessExited { status }
+                match status {
+                    Ok(status) => {
+                        self.child = None;
+                        ProcessExited { status }
+                    },
+                    Err(err) => {
+                        let () = futures_util::future::pending().await;
+                        ProcessWaitFailed { err }
+                    }
+                }
             }
         };
 
@@ -208,12 +217,12 @@ impl AppBuilder {
                             self.bundling_progress = 0.0;
                         }
                         BuildStage::Starting { crate_count, .. } => {
-                            self.expected_crates = *crate_count;
+                            self.expected_crates = *crate_count.max(&1);
                         }
                         BuildStage::InstallingTooling => {}
                         BuildStage::Compiling { current, total, .. } => {
                             self.compiled_crates = *current;
-                            self.expected_crates = *total;
+                            self.expected_crates = *total.max(&1);
 
                             if self.compile_start.is_none() {
                                 self.compile_start = Some(Instant::now());
@@ -263,6 +272,7 @@ impl AppBuilder {
             StdoutReceived { .. } => {}
             StderrReceived { .. } => {}
             ProcessExited { .. } => {}
+            ProcessWaitFailed { .. } => {}
         }
 
         update
@@ -380,7 +390,7 @@ impl AppBuilder {
                     tracing::info!(json = ?StructuredOutput::BuildUpdate { stage: stage.clone() });
                 }
                 BuilderUpdate::CompilerMessage { message } => {
-                    tracing::info!(json = ?StructuredOutput::CargoOutput { message: message.clone() }, %message);
+                    tracing::info!(json = ?StructuredOutput::RustcOutput { message: message.clone() }, %message);
                 }
                 BuilderUpdate::BuildReady { bundle } => {
                     tracing::debug!(json = ?StructuredOutput::BuildFinished {
@@ -392,7 +402,7 @@ impl AppBuilder {
                     // Flush remaining compiler messages
                     while let Ok(Some(msg)) = self.rx.try_next() {
                         if let BuilderUpdate::CompilerMessage { message } = msg {
-                            tracing::info!(json = ?StructuredOutput::CargoOutput { message: message.clone() }, %message);
+                            tracing::info!(json = ?StructuredOutput::RustcOutput { message: message.clone() }, %message);
                         }
                     }
 
@@ -402,33 +412,25 @@ impl AppBuilder {
                 BuilderUpdate::StdoutReceived { .. } => {}
                 BuilderUpdate::StderrReceived { .. } => {}
                 BuilderUpdate::ProcessExited { .. } => {}
+                BuilderUpdate::ProcessWaitFailed { .. } => {}
             }
         }
     }
 
-    pub(crate) async fn open(
+    /// Create a list of environment variables that the child process will use
+    pub(crate) fn child_environment_variables(
         &mut self,
-        devserver_ip: SocketAddr,
-        open_address: Option<SocketAddr>,
+        devserver_ip: Option<SocketAddr>,
         start_fullstack_on_address: Option<SocketAddr>,
-        open_browser: bool,
         always_on_top: bool,
         build_id: BuildId,
-    ) -> Result<()> {
+    ) -> Vec<(&'static str, String)> {
         let krate = &self.build;
 
         // 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_config::CLI_ENABLED_ENV, "true".to_string()),
-            (
-                dioxus_cli_config::DEVSERVER_IP_ENV,
-                devserver_ip.ip().to_string(),
-            ),
-            (
-                dioxus_cli_config::DEVSERVER_PORT_ENV,
-                devserver_ip.port().to_string(),
-            ),
             (
                 dioxus_cli_config::APP_TITLE_ENV,
                 krate.config.web.app.title.clone(),
@@ -442,12 +444,27 @@ impl AppBuilder {
                 dioxus_cli_config::ALWAYS_ON_TOP_ENV,
                 always_on_top.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()),
-            ("RUST_BACKTRACE", "1".to_string()),
         ];
 
+        if let Some(devserver_ip) = devserver_ip {
+            envs.push((
+                dioxus_cli_config::DEVSERVER_IP_ENV,
+                devserver_ip.ip().to_string(),
+            ));
+            envs.push((
+                dioxus_cli_config::DEVSERVER_PORT_ENV,
+                devserver_ip.port().to_string(),
+            ));
+        }
+
+        if crate::VERBOSITY
+            .get()
+            .map(|f| f.verbose)
+            .unwrap_or_default()
+        {
+            envs.push(("RUST_BACKTRACE", "1".to_string()));
+        }
+
         if let Some(base_path) = krate.base_path() {
             envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.to_string()));
         }
@@ -463,8 +480,29 @@ impl AppBuilder {
             envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()));
         }
 
+        envs
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    pub(crate) async fn open(
+        &mut self,
+        devserver_ip: SocketAddr,
+        open_address: Option<SocketAddr>,
+        start_fullstack_on_address: Option<SocketAddr>,
+        open_browser: bool,
+        always_on_top: bool,
+        build_id: BuildId,
+        args: &[String],
+    ) -> Result<()> {
+        let envs = self.child_environment_variables(
+            Some(devserver_ip),
+            start_fullstack_on_address,
+            always_on_top,
+            build_id,
+        );
+
         // We try to use stdin/stdout to communicate with the app
-        let running_process = match self.build.platform {
+        match self.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.
@@ -473,15 +511,19 @@ impl AppBuilder {
                 if open_browser {
                     self.open_web(open_address.unwrap_or(devserver_ip));
                 }
-
-                None
             }
 
-            Platform::Ios => Some(self.open_ios_sim(envs).await?),
+            Platform::Ios => {
+                if self.build.device {
+                    self.codesign_ios().await?;
+                    self.open_ios_device().await?
+                } else {
+                    self.open_ios_sim(envs).await?
+                }
+            }
 
             Platform::Android => {
                 self.open_android_sim(false, devserver_ip, envs).await?;
-                None
             }
 
             // These are all just basically running the main exe, but with slightly different resource dir paths
@@ -489,18 +531,9 @@ impl AppBuilder {
             | Platform::MacOS
             | Platform::Windows
             | Platform::Linux
-            | Platform::Liveview => Some(self.open_with_main_exe(envs)?),
+            | Platform::Liveview => self.open_with_main_exe(envs, args)?,
         };
 
-        // 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.stdout = Some(stdout.lines());
-            self.stderr = Some(stderr.lines());
-            self.child = Some(child);
-        }
-
         self.builds_opened += 1;
 
         Ok(())
@@ -561,19 +594,23 @@ impl AppBuilder {
         let original = self.build.main_exe();
         let new = self.build.patch_exe(res.time_start);
         let triple = self.build.triple.clone();
-        let original_artifacts = self.artifacts.as_ref().unwrap();
         let asset_dir = self.build.asset_dir();
 
-        for (k, bundled) in res.assets.assets.iter() {
-            let k = dunce::canonicalize(k)?;
-            if original_artifacts.assets.assets.contains_key(k.as_path()) {
+        for bundled in res.assets.assets() {
+            let original_artifacts = self.artifacts.as_mut().unwrap();
+
+            if original_artifacts.assets.contains(bundled) {
                 continue;
             }
 
-            let from = k.clone();
+            // If this is a new asset, insert it into the artifacts so we can track it when hot reloading
+            original_artifacts.assets.insert_asset(*bundled);
+
+            let from = dunce::canonicalize(PathBuf::from(bundled.absolute_source_path()))?;
+
             let to = asset_dir.join(bundled.bundled_path());
 
-            tracing::debug!("Copying asset from patch: {}", k.display());
+            tracing::debug!("Copying asset from patch: {}", from.display());
             if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
                 tracing::error!("Failed to copy asset: {e}");
                 continue;
@@ -581,13 +618,8 @@ impl AppBuilder {
 
             // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
             if self.build.platform == Platform::Android {
-                let changed_file = dunce::canonicalize(k).inspect_err(|e| {
-                    tracing::debug!("Failed to canonicalize hotreloaded asset: {e}")
-                })?;
                 let bundled_name = PathBuf::from(bundled.bundled_path());
-                _ = self
-                    .copy_file_to_android_tmp(&changed_file, &bundled_name)
-                    .await;
+                _ = self.copy_file_to_android_tmp(&from, &bundled_name).await;
             }
         }
 
@@ -617,13 +649,15 @@ impl AppBuilder {
             _ => vec![],
         };
 
+        use crate::styles::{GLOW_STYLE, NOTE_STYLE};
+
         let changed_file = changed_files.first().unwrap();
         tracing::info!(
-            "Hot-patching: {} took {:?}ms",
+            "Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
             changed_file
-                .strip_prefix(std::env::current_dir().unwrap())
-                .unwrap_or(changed_file.as_path())
-                .display(),
+                .display()
+                .to_string()
+                .trim_start_matches(&self.build.crate_dir().display().to_string()),
             SystemTime::now()
                 .duration_since(res.time_start)
                 .unwrap()
@@ -644,10 +678,13 @@ impl AppBuilder {
     /// 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
+    /// This will return the bundled name of the assets 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) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option<PathBuf> {
+    pub(crate) async fn hotreload_bundled_assets(
+        &self,
+        changed_file: &PathBuf,
+    ) -> Option<Vec<PathBuf>> {
         let artifacts = self.artifacts.as_ref()?;
 
         // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
@@ -663,32 +700,36 @@ impl AppBuilder {
             .ok()?;
 
         // The asset might've been renamed thanks to the manifest, let's attempt to reload that too
-        let resource = artifacts.assets.assets.get(&changed_file)?;
-        let output_path = asset_dir.join(resource.bundled_path());
-
-        tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
-
-        // Remove the old asset if it exists
-        _ = std::fs::remove_file(&output_path);
-
-        // And then process the asset with the options into the **old** asset location. If we recompiled,
-        // the asset would be in a new location because the contents and hash have changed. Since we are
-        // hotreloading, we need to use the old asset location it was originally written to.
-        let options = *resource.options();
-        let res = process_file_to(&options, &changed_file, &output_path);
-        let bundled_name = PathBuf::from(resource.bundled_path());
-        if let Err(e) = res {
-            tracing::debug!("Failed to hotreload asset {e}");
-        }
+        let resources = artifacts.assets.get_assets_for_source(&changed_file)?;
+        let mut bundled_names = Vec::new();
+        for resource in resources {
+            let output_path = asset_dir.join(resource.bundled_path());
+
+            tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
+
+            // Remove the old asset if it exists
+            _ = std::fs::remove_file(&output_path);
+
+            // And then process the asset with the options into the **old** asset location. If we recompiled,
+            // the asset would be in a new location because the contents and hash have changed. Since we are
+            // hotreloading, we need to use the old asset location it was originally written to.
+            let options = *resource.options();
+            let res = process_file_to(&options, &changed_file, &output_path);
+            let bundled_name = PathBuf::from(resource.bundled_path());
+            if let Err(e) = res {
+                tracing::debug!("Failed to hotreload asset {e}");
+            }
 
-        // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
-        if self.build.platform == Platform::Android {
-            _ = self
-                .copy_file_to_android_tmp(&changed_file, &bundled_name)
-                .await;
+            // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
+            if self.build.platform == Platform::Android {
+                _ = self
+                    .copy_file_to_android_tmp(&changed_file, &bundled_name)
+                    .await;
+            }
+            bundled_names.push(bundled_name);
         }
 
-        Some(bundled_name)
+        Some(bundled_names)
     }
 
     /// Copy this file to the tmp folder on the android device, returning the path to the copied file
@@ -704,7 +745,7 @@ impl AppBuilder {
         let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name);
         tracing::debug!("Pushing asset to device: {target:?}");
 
-        let res = tokio::process::Command::new(&self.build.workspace.android_tools()?.adb)
+        let res = Command::new(&self.build.workspace.android_tools()?.adb)
             .arg("push")
             .arg(changed_file)
             .arg(&target)
@@ -726,29 +767,37 @@ impl AppBuilder {
     /// 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<Child> {
+    fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>, args: &[String]) -> Result<()> {
         let main_exe = self.app_exe();
 
         tracing::debug!("Opening app with main exe: {main_exe:?}");
 
-        let child = Command::new(main_exe)
+        let mut child = Command::new(main_exe)
+            .args(args)
             .envs(envs)
+            .env_remove("CARGO_MANIFEST_DIR") // running under `dx` shouldn't expose cargo-only :
             .stderr(Stdio::piped())
             .stdout(Stdio::piped())
             .kill_on_drop(true)
             .spawn()?;
 
-        Ok(child)
+        let stdout = BufReader::new(child.stdout.take().unwrap());
+        let stderr = BufReader::new(child.stderr.take().unwrap());
+        self.stdout = Some(stdout.lines());
+        self.stderr = Some(stderr.lines());
+        self.child = Some(child);
+
+        Ok(())
     }
 
     /// 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, address: SocketAddr) {
-        let base_path = self.build.config.web.app.base_path.clone();
+        let base_path = self.build.base_path();
         let https = self.build.config.web.https.enabled.unwrap_or_default();
         let protocol = if https { "https" } else { "http" };
-        let base_path = match base_path.as_deref() {
+        let base_path = match base_path {
             Some(base_path) => format!("/{}", base_path.trim_matches('/')),
             None => "".to_owned(),
         };
@@ -763,7 +812,7 @@ impl AppBuilder {
     ///
     /// 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<Child> {
+    async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<()> {
         tracing::debug!("Installing app to simulator {:?}", self.build.root_dir());
 
         let res = Command::new("xcrun")
@@ -782,19 +831,26 @@ impl AppBuilder {
             .iter()
             .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone()));
 
-        let child = Command::new("xcrun")
+        let mut child = Command::new("xcrun")
             .arg("simctl")
             .arg("launch")
             .arg("--console")
             .arg("booted")
             .arg(self.build.bundle_identifier())
             .envs(ios_envs)
+            .env_remove("CARGO_MANIFEST_DIR")
             .stderr(Stdio::piped())
             .stdout(Stdio::piped())
             .kill_on_drop(true)
             .spawn()?;
 
-        Ok(child)
+        let stdout = BufReader::new(child.stdout.take().unwrap());
+        let stderr = BufReader::new(child.stderr.take().unwrap());
+        self.stdout = Some(stdout.lines());
+        self.stderr = Some(stderr.lines());
+        self.child = Some(child);
+
+        Ok(())
     }
 
     /// We have this whole thing figured out, but we don't actually use it yet.
@@ -804,54 +860,35 @@ impl AppBuilder {
     ///
     /// 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<()> {
         use serde_json::Value;
-        let app_path = self.build.root_dir();
-
-        install_app(&app_path).await?;
 
-        // 2. Determine which device the app was installed to
+        // 1. Find an active device
         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?;
+        // 2. Get the installation URL of the app
+        let installation_url = get_installation_url(&device_uuid, &self.build.root_dir()).await?;
 
-        // 4. Launch the app into the background, paused
+        // 3. 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<String> {
-            let output = Command::new("xcrun")
+            let tmpfile = tempfile::NamedTempFile::new()
+                .context("Failed to create temporary file for device list")?;
+
+            Command::new("xcrun")
                 .args([
-                    "devicectl",
-                    "list",
-                    "devices",
-                    "--json-output",
-                    "target/deviceid.json",
+                    "devicectl".to_string(),
+                    "list".to_string(),
+                    "devices".to_string(),
+                    "--json-output".to_string(),
+                    tmpfile.path().to_str().unwrap().to_string(),
                 ])
                 .output()
                 .await?;
 
-            let json: Value =
-                serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?)
-                    .context("Failed to parse xcrun output")?;
+            let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
+                .context("Failed to parse xcrun output")?;
             let device_uuid = json["result"]["devices"][0]["identifier"]
                 .as_str()
                 .ok_or("Failed to extract device UUID")?
@@ -861,6 +898,9 @@ impl AppBuilder {
         }
 
         async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
+            let tmpfile = tempfile::NamedTempFile::new()
+                .context("Failed to create temporary file for device list")?;
+
             // xcrun devicectl device install app --device <uuid> --path <path> --json-output
             let output = Command::new("xcrun")
                 .args([
@@ -872,8 +912,8 @@ impl AppBuilder {
                     device_uuid,
                     &app_path.display().to_string(),
                     "--json-output",
-                    "target/xcrun.json",
                 ])
+                .arg(tmpfile.path())
                 .output()
                 .await?;
 
@@ -881,7 +921,7 @@ impl AppBuilder {
                 return Err(format!("Failed to install app: {:?}", output).into());
             }
 
-            let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?)
+            let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
                 .context("Failed to parse xcrun output")?;
             let installation_url = json["result"]["installedApplications"][0]["installationURL"]
                 .as_str()
@@ -892,6 +932,9 @@ impl AppBuilder {
         }
 
         async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
+            let tmpfile = tempfile::NamedTempFile::new()
+                .context("Failed to create temporary file for device list")?;
+
             let output = Command::new("xcrun")
                 .args([
                     "devicectl",
@@ -904,8 +947,8 @@ impl AppBuilder {
                     device_uuid,
                     installation_url,
                     "--json-output",
-                    "target/launch.json",
                 ])
+                .arg(tmpfile.path())
                 .output()
                 .await?;
 
@@ -913,11 +956,7 @@ impl AppBuilder {
                 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")?)
+            let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
                 .context("Failed to parse xcrun output")?;
 
             let status_pid = json["result"]["process"]["processIdentifier"]
@@ -945,10 +984,9 @@ impl AppBuilder {
             Ok(())
         }
 
-        unimplemented!("dioxus-cli doesn't support ios devices yet.")
+        Ok(())
     }
 
-    #[allow(unused)]
     async fn codesign_ios(&self) -> Result<()> {
         const CODESIGN_ERROR: &str = r#"This is likely because you haven't
 - Created a provisioning profile before
@@ -962,7 +1000,7 @@ https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certi
 
         let profiles_folder = dirs::home_dir()
             .context("Your machine has no home-dir")?
-            .join("Library/MobileDevice/Provisioning Profiles");
+            .join("Library/Developer/Xcode/UserData/Provisioning Profiles");
 
         if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
             tracing::error!(
@@ -1031,10 +1069,11 @@ We checked the folder: {}
         struct ProvisioningProfile {
             #[serde(rename = "TeamIdentifier")]
             team_identifier: Vec<String>,
-            #[serde(rename = "ApplicationIdentifierPrefix")]
-            application_identifier_prefix: Vec<String>,
             #[serde(rename = "Entitlements")]
             entitlements: Entitlements,
+            #[allow(dead_code)]
+            #[serde(rename = "ApplicationIdentifierPrefix")]
+            application_identifier_prefix: Vec<String>,
         }
 
         #[derive(serde::Deserialize, Debug)]
@@ -1127,7 +1166,7 @@ We checked the folder: {}
     /// - If the app fails to launch, errors are logged for debugging purposes.
     ///
     /// # Resources:
-    /// - https://developer.android.com/studio/run/emulator-commandline
+    /// - <https://developer.android.com/studio/run/emulator-commandline>
     async fn open_android_sim(
         &self,
         root: bool,
@@ -1136,11 +1175,11 @@ We checked the folder: {}
     ) -> Result<()> {
         let apk_path = self.build.debug_apk_path();
         let session_cache = self.build.session_cache_dir();
-        let full_mobile_app_name = self.build.full_mobile_app_name();
+        let application_id = self.build.bundle_identifier();
         let adb = self.build.workspace.android_tools()?.adb.clone();
 
         // Start backgrounded since .open() is called while in the arm of the top-level match
-        tokio::task::spawn(async move {
+        let _handle: JoinHandle<Result<()>> = tokio::task::spawn(async move {
             // call `adb root` so we can push patches to the device
             if root {
                 if let Err(e) = Command::new(&adb).arg("root").output().await {
@@ -1159,54 +1198,79 @@ We checked the folder: {}
                 tracing::error!("failed to forward port {port}: {e}");
             }
 
+            // Wait for device to be ready
+            let cmd = Command::new(&adb)
+                .arg("wait-for-device")
+                .arg("shell")
+                .arg(r#"while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"#)
+                .output();
+            let cmd_future = cmd.fuse();
+            pin_mut!(cmd_future);
+            tokio::select! {
+                _ = &mut cmd_future => {}
+                _ = tokio::time::sleep(Duration::from_millis(50)) => {
+                    tracing::info!("Waiting for android emulator to be ready...");
+                    _ = cmd_future.await;
+                }
+            }
+
             // Install
             // adb install -r app-debug.apk
-            if let Err(e) = Command::new(&adb)
+            let res = Command::new(&adb)
                 .arg("install")
                 .arg("-r")
                 .arg(apk_path)
                 .output()
-                .await
-            {
-                tracing::error!("Failed to install apk with `adb`: {e}");
-            };
+                .await?;
+            let std_err = String::from_utf8_lossy(&res.stderr);
+            if !std_err.is_empty() {
+                tracing::error!("Failed to install apk with `adb`: {std_err}");
+            }
+
+            // Clear the session cache dir on the device
+            Command::new(&adb)
+                .arg("shell")
+                .arg("rm")
+                .arg("-rf")
+                .arg(dioxus_cli_config::android_session_cache_dir())
+                .output()
+                .await?;
 
             // Write the env vars to a .env file in our session cache
             let env_file = session_cache.join(".env");
-            let contents: String = envs
-                .iter()
-                .map(|(key, value)| format!("{key}={value}"))
-                .collect::<Vec<_>>()
-                .join("\n");
-            _ = std::fs::write(&env_file, contents);
+            _ = std::fs::write(
+                &env_file,
+                envs.iter()
+                    .map(|(key, value)| format!("{key}={value}"))
+                    .collect::<Vec<_>>()
+                    .join("\n"),
+            );
 
             // Push the env file to the device
-            if let Err(e) = tokio::process::Command::new(&adb)
+            Command::new(&adb)
                 .arg("push")
                 .arg(env_file)
                 .arg(dioxus_cli_config::android_session_cache_dir().join(".env"))
                 .output()
-                .await
-                .context("Failed to push asset to device")
-            {
-                tracing::error!("Failed to push .env file to device: {e}");
-            }
+                .await?;
 
             // eventually, use the user's MainActivity, not our MainActivity
             // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
-            let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
-
-            if let Err(e) = Command::new(&adb)
+            let activity_name = format!("{application_id}/dev.dioxus.main.MainActivity");
+            let res = Command::new(&adb)
                 .arg("shell")
                 .arg("am")
                 .arg("start")
                 .arg("-n")
                 .arg(activity_name)
                 .output()
-                .await
-            {
-                tracing::error!("Failed to start app with `adb`: {e}");
-            };
+                .await?;
+            let std_err = String::from_utf8_lossy(res.stderr.trim_ascii());
+            if !std_err.is_empty() {
+                tracing::error!("Failed to start app with `adb`: {std_err}");
+            }
+
+            Ok(())
         });
 
         Ok(())
@@ -1312,4 +1376,173 @@ We checked the folder: {}
     pub(crate) fn can_receive_hotreloads(&self) -> bool {
         matches!(&self.stage, BuildStage::Success | BuildStage::Failed)
     }
+
+    pub(crate) async fn open_debugger(&mut self, server: &WebServer) -> Result<()> {
+        let url = match self.build.platform {
+            Platform::MacOS
+            | Platform::Windows
+            | Platform::Linux
+            | Platform::Server
+            | Platform::Liveview => {
+                let Some(Some(pid)) = self.child.as_mut().map(|f| f.id()) else {
+                    tracing::warn!("No process to attach debugger to");
+                    return Ok(());
+                };
+
+                format!(
+                    "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{}}}",
+                    pid
+                )
+            }
+
+            Platform::Web => {
+                // code --open-url "vscode://DioxusLabs.dioxus/debugger?uri=http://127.0.0.1:8080"
+                // todo - debugger could open to the *current* page afaik we don't have a way to have that info
+                let address = server.devserver_address();
+                let base_path = self.build.base_path();
+                let https = self.build.config.web.https.enabled.unwrap_or_default();
+                let protocol = if https { "https" } else { "http" };
+                let base_path = match base_path {
+                    Some(base_path) => format!("/{}", base_path.trim_matches('/')),
+                    None => "".to_owned(),
+                };
+                format!("vscode://DioxusLabs.dioxus/debugger?uri={protocol}://{address}{base_path}")
+            }
+
+            Platform::Ios => {
+                let Some(pid) = self.pid else {
+                    tracing::warn!("No process to attach debugger to");
+                    return Ok(());
+                };
+
+                format!(
+                    "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{pid}}}"
+                )
+            }
+
+            // https://stackoverflow.com/questions/53733781/how-do-i-use-lldb-to-debug-c-code-on-android-on-command-line/64997332#64997332
+            // https://android.googlesource.com/platform/development/+/refs/heads/main/scripts/gdbclient.py
+            // run lldbserver on the device and then connect
+            //
+            // # TODO: https://code.visualstudio.com/api/references/vscode-api#debug and
+            // #       https://code.visualstudio.com/api/extension-guides/debugger-extension and
+            // #       https://github.com/vadimcn/vscode-lldb/blob/6b775c439992b6615e92f4938ee4e211f1b060cf/extension/pickProcess.ts#L6
+            //
+            // res = {
+            //     "name": "(lldbclient.py) Attach {} (port: {})".format(binary_name.split("/")[-1], port),
+            //     "type": "lldb",
+            //     "request": "custom",
+            //     "relativePathBase": root,
+            //     "sourceMap": { "/b/f/w" : root, '': root, '.': root },
+            //     "initCommands": ['settings append target.exec-search-paths {}'.format(' '.join(solib_search_path))],
+            //     "targetCreateCommands": ["target create {}".format(binary_name),
+            //                              "target modules search-paths add / {}/".format(sysroot)],
+            //     "processCreateCommands": ["gdb-remote {}".format(str(port))]
+            // }
+            //
+            // https://github.com/vadimcn/codelldb/issues/213
+            //
+            // lots of pain to figure this out:
+            //
+            // (lldb) image add target/dx/tw6/debug/android/app/app/src/main/jniLibs/arm64-v8a/libdioxusmain.so
+            // (lldb) settings append target.exec-search-paths target/dx/tw6/debug/android/app/app/src/main/jniLibs/arm64-v8a/libdioxusmain.so
+            // (lldb) process handle SIGSEGV --pass true --stop false --notify true (otherwise the java threads cause crash)
+            //
+            Platform::Android => {
+                // adb push ./sdk/ndk/29.0.13113456/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/20/lib/linux/aarch64/lldb-server /tmp
+                // adb shell "/tmp/lldb-server --server --listen ..."
+                // "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'connect','port': {}}}",
+                // format!(
+                //     "vscode://vadimcn.vscode-lldb/launch/config?{{'request':'attach','pid':{pid}}}"
+                // )
+                let tools = &self.build.workspace.android_tools()?;
+
+                // get the pid of the app
+                let pid = Command::new(&tools.adb)
+                    .arg("shell")
+                    .arg("pidof")
+                    .arg(self.build.bundle_identifier())
+                    .output()
+                    .await
+                    .ok()
+                    .and_then(|output| String::from_utf8(output.stdout).ok())
+                    .and_then(|s| s.trim().parse::<u32>().ok())
+                    .unwrap();
+
+                // copy the lldb-server to the device
+                let lldb_server = tools
+                    .android_tools_dir()
+                    .parent()
+                    .unwrap()
+                    .join("lib")
+                    .join("clang")
+                    .join("20")
+                    .join("lib")
+                    .join("linux")
+                    .join("aarch64")
+                    .join("lldb-server");
+
+                tracing::info!("Copying lldb-server to device: {lldb_server:?}");
+
+                _ = Command::new(&tools.adb)
+                    .arg("push")
+                    .arg(lldb_server)
+                    .arg("/tmp/lldb-server")
+                    .output()
+                    .await;
+
+                // Forward requests on 10086 to the device
+                _ = Command::new(&tools.adb)
+                    .arg("forward")
+                    .arg("tcp:10086")
+                    .arg("tcp:10086")
+                    .output()
+                    .await;
+
+                // start the server - running it multiple times will make the subsequent ones fail (which is fine)
+                _ = Command::new(&tools.adb)
+                    .arg("shell")
+                    .arg(r#"cd /tmp && ./lldb-server platform --server --listen '*:10086'"#)
+                    .kill_on_drop(false)
+                    .stdin(Stdio::null())
+                    .stdout(Stdio::piped())
+                    .stderr(Stdio::piped())
+                    .spawn();
+
+                let program_path = self.build.main_exe();
+                format!(
+                    r#"vscode://vadimcn.vscode-lldb/launch/config?{{
+                        'name':'Attach to Android',
+                        'type':'lldb',
+                        'request':'attach',
+                        'pid': '{pid}',
+                        'processCreateCommands': [
+                            'platform select remote-android',
+                            'platform connect connect://localhost:10086',
+                            'settings set target.inherit-env false',
+                            'settings set target.inline-breakpoint-strategy always',
+                            'settings set target.process.thread.step-avoid-regexp \"JavaBridge|JDWP|Binder|ReferenceQueueDaemon\"',
+                            'process handle SIGSEGV --pass true --stop false --notify true"',
+                            'settings append target.exec-search-paths {program_path}',
+                            'attach --pid {pid}',
+                            'continue'
+                        ]
+                    }}"#,
+                    program_path = program_path.display(),
+                )
+                .lines()
+                .map(|line| line.trim())
+                .join("")
+            }
+        };
+
+        tracing::info!("Opening debugger for [{}]: {url}", self.build.platform);
+
+        _ = tokio::process::Command::new("code")
+            .arg("--open-url")
+            .arg(url)
+            .spawn();
+
+        Ok(())
+    }
 }

+ 9 - 3
packages/cli/src/build/context.rs

@@ -2,7 +2,7 @@
 
 use super::BuildMode;
 use crate::{BuildArtifacts, BuildStage, Error, TraceSrc};
-use cargo_metadata::CompilerMessage;
+use cargo_metadata::diagnostic::Diagnostic;
 use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
 use serde::{Deserialize, Serialize};
 use std::{path::PathBuf, process::ExitStatus};
@@ -35,7 +35,7 @@ pub enum BuilderUpdate {
     },
 
     CompilerMessage {
-        message: CompilerMessage,
+        message: Diagnostic,
     },
 
     BuildReady {
@@ -65,6 +65,12 @@ pub enum BuilderUpdate {
     ProcessExited {
         status: ExitStatus,
     },
+
+    /// Waiting for the process failed. This might be because it's hung or being debugged.
+    /// This is not the same as the process exiting, so it should just be logged but not treated as an error.
+    ProcessWaitFailed {
+        err: std::io::Error,
+    },
 }
 
 impl BuildContext {
@@ -92,7 +98,7 @@ impl BuildContext {
         })
     }
 
-    pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) {
+    pub(crate) fn status_build_diagnostic(&self, message: Diagnostic) {
         _ = self
             .tx
             .unbounded_send(BuilderUpdate::CompilerMessage { message });

+ 4 - 0
packages/cli/src/build/mod.rs

@@ -8,14 +8,18 @@
 //! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains
 //! the management of the ongoing build and methods to open the build as a running app.
 
+mod assets;
 mod builder;
 mod context;
 mod patch;
+mod pre_render;
 mod request;
 mod tools;
 
+pub(crate) use assets::*;
 pub(crate) use builder::*;
 pub(crate) use context::*;
 pub(crate) use patch::*;
+pub(crate) use pre_render::*;
 pub(crate) use request::*;
 pub(crate) use tools::*;

+ 162 - 45
packages/cli/src/build/patch.rs

@@ -3,8 +3,8 @@ use itertools::Itertools;
 use object::{
     macho::{self},
     read::File,
-    write::{MachOBuildVersion, StandardSection, Symbol, SymbolSection},
-    Endianness, Object, ObjectSymbol, SymbolKind, SymbolScope,
+    write::{MachOBuildVersion, SectionId, StandardSection, Symbol, SymbolId, SymbolSection},
+    Endianness, Object, ObjectSymbol, SymbolFlags, SymbolKind, SymbolScope,
 };
 use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
 use std::{
@@ -15,7 +15,7 @@ use std::{
     sync::{Arc, RwLock},
 };
 use subsecond_types::{AddressMap, JumpTable};
-use target_lexicon::{Architecture, OperatingSystem, Triple};
+use target_lexicon::{Architecture, OperatingSystem, PointerWidth, Triple};
 use thiserror::Error;
 use walrus::{
     ConstExpr, DataKind, ElementItems, ElementKind, FunctionBuilder, FunctionId, FunctionKind,
@@ -83,6 +83,8 @@ pub struct CachedSymbol {
     pub kind: SymbolKind,
     pub is_undefined: bool,
     pub is_weak: bool,
+    pub size: u64,
+    pub flags: SymbolFlags<SectionId, SymbolId>,
 }
 
 impl PartialEq for HotpatchModuleCache {
@@ -137,6 +139,8 @@ impl HotpatchModuleCache {
                                     },
                                     is_undefined,
                                     is_weak: false,
+                                    size: 0,
+                                    flags: SymbolFlags::None,
                                 },
                             );
                         }
@@ -155,6 +159,8 @@ impl HotpatchModuleCache {
                                     kind: SymbolKind::Data,
                                     is_undefined,
                                     is_weak: false,
+                                    size: 0,
+                                    flags: SymbolFlags::None,
                                 },
                             );
                         }
@@ -249,6 +255,15 @@ impl HotpatchModuleCache {
                 let symbol_table = obj
                     .symbols()
                     .filter_map(|s| {
+                        let flags = match s.flags() {
+                            SymbolFlags::None => SymbolFlags::None,
+                            SymbolFlags::Elf { st_info, st_other } => {
+                                SymbolFlags::Elf { st_info, st_other }
+                            }
+                            SymbolFlags::MachO { n_desc } => SymbolFlags::MachO { n_desc },
+                            _ => SymbolFlags::None,
+                        };
+
                         Some((
                             s.name().ok()?.to_string(),
                             CachedSymbol {
@@ -256,6 +271,8 @@ impl HotpatchModuleCache {
                                 is_undefined: s.is_undefined(),
                                 is_weak: s.is_weak(),
                                 kind: s.kind(),
+                                size: s.size(),
+                                flags,
                             },
                         ))
                     })
@@ -322,9 +339,9 @@ fn create_windows_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Resul
         .context("failed to find 'main' symbol in patch")?;
 
     let aslr_reference = old_name_to_addr
-        .get("__aslr_reference")
+        .get("main")
         .map(|s| s.address)
-        .context("failed to find '_aslr_reference' symbol in original module")?;
+        .context("failed to find '_main' symbol in original module")?;
 
     Ok(JumpTable {
         lib: patch.to_path_buf(),
@@ -367,31 +384,15 @@ fn create_native_jump_table(
         }
     }
 
-    let new_base_address = match triple.operating_system {
-        // The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
-        OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
-            *new_name_to_addr
-                .get("_main")
-                .context("failed to find '_main' symbol in patch")?
-        }
-
-        // No distincation between the two on these platforms
-        OperatingSystem::Freebsd
-        | OperatingSystem::Openbsd
-        | OperatingSystem::Linux
-        | OperatingSystem::Windows => *new_name_to_addr
-            .get("main")
-            .context("failed to find 'main' symbol in patch")?,
-
-        // On wasm, it doesn't matter what the address is since the binary is PIC
-        _ => 0,
-    };
-
+    let sentinel = main_sentinel(triple);
+    let new_base_address = new_name_to_addr
+        .get(sentinel)
+        .cloned()
+        .context("failed to find 'main' symbol in base - are deubg symbols enabled?")?;
     let aslr_reference = old_name_to_addr
-        .get("___aslr_reference")
-        .or_else(|| old_name_to_addr.get("__aslr_reference"))
+        .get(sentinel)
         .map(|s| s.address)
-        .context("failed to find '___aslr_reference' symbol in original module")?;
+        .context("failed to find 'main' symbol in original module - are debug symbols enabled?")?;
 
     Ok(JumpTable {
         lib: patch.to_path_buf(),
@@ -414,7 +415,7 @@ fn create_native_jump_table(
 /// It doesn't seem like we can compile the base module to export these, sadly, so we're going
 /// to manually satisfy them here, removing their need to be imported.
 ///
-/// https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
+/// <https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md>
 fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Result<JumpTable> {
     let name_to_ifunc_old = &cache.symbol_ifunc_map;
     let old = &cache.old_wasm;
@@ -596,26 +597,41 @@ fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Result<J
         {
             continue;
         }
+        let name = import.name.as_str().to_string();
 
         if let Some(table_idx) = name_to_ifunc_old.get(import.name.as_str()) {
-            let name = import.name.as_str().to_string();
             new.imports.delete(env_func_import);
             convert_import_to_ifunc_call(
                 &mut new,
                 ifunc_table_initializer,
                 func_id,
                 *table_idx,
-                name,
+                name.clone(),
             );
         }
+
+        if name_is_bindgen_symbol(&name) {
+            new.imports.delete(env_func_import);
+            convert_import_to_ifunc_call(&mut new, ifunc_table_initializer, func_id, 0, name);
+        }
     }
 
     // Wire up the preserved intrinsic functions that we saved before running wasm-bindgen to the expected
     // imports from the patch.
     for import_id in wbg_funcs {
         let import = new.imports.get_mut(import_id);
+        let ImportKind::Function(func_id) = import.kind else {
+            continue;
+        };
+
         import.module = "env".into();
         import.name = format!("__saved_wbg_{}", import.name);
+
+        if name_is_bindgen_symbol(&import.name) {
+            let name = import.name.as_str().to_string();
+            new.imports.delete(import_id);
+            convert_import_to_ifunc_call(&mut new, ifunc_table_initializer, func_id, 0, name);
+        }
     }
 
     // Wipe away the unnecessary sections
@@ -827,22 +843,31 @@ pub fn create_undefined_symbol_stub(
         _ => {}
     }
 
-    let symbol_table = &cache.symbol_table;
+    // Get the offset from the main module and adjust the addresses by the slide;
+    let aslr_ref_address = cache
+        .symbol_table
+        .get(main_sentinel(triple))
+        .context("failed to find '_main' symbol in patch")?
+        .address;
+
+    if aslr_reference < aslr_ref_address {
+        return Err(PatchError::InvalidModule(
+            format!(
+            "ASLR reference is less than the main module's address - is there a `main`?. {:x} < {:x}", aslr_reference, aslr_ref_address )
+        ));
+    }
 
-    // Get the offset from the main module and adjust the addresses by the slide
-    let aslr_ref_address = symbol_table
-        .get("___aslr_reference")
-        .or_else(|| symbol_table.get("__aslr_reference"))
-        .map(|s| s.address)
-        .context("Failed to find ___aslr_reference symbol")?;
     let aslr_offset = aslr_reference - aslr_ref_address;
 
     // we need to assemble a PLT/GOT so direct calls to the patch symbols work
     // for each symbol we either write the address directly (as a symbol) or create a PLT/GOT entry
     let text_section = obj.section_id(StandardSection::Text);
     for name in undefined_symbols {
-        let Some(sym) = symbol_table.get(name.as_str().trim_start_matches("__imp_")) else {
-            tracing::error!("Symbol not found: {}", name);
+        let Some(sym) = cache
+            .symbol_table
+            .get(name.as_str().trim_start_matches("__imp_"))
+        else {
+            tracing::debug!("Symbol not found: {}", name);
             continue;
         };
 
@@ -895,7 +920,7 @@ pub fn create_undefined_symbol_stub(
                     kind: SymbolKind::Data, // Always Data for IAT entries
                     weak: false,
                     section: SymbolSection::Section(data_section),
-                    flags: object::SymbolFlags::None,
+                    flags: SymbolFlags::None,
                 });
             }
 
@@ -1015,7 +1040,6 @@ pub fn create_undefined_symbol_stub(
                         _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())),
                     },
                 };
-
                 let offset = obj.append_section_data(text_section, &jump_asm, 8);
                 obj.add_symbol(Symbol {
                     name: name.as_bytes()[name_offset..].to_vec(),
@@ -1025,7 +1049,76 @@ pub fn create_undefined_symbol_stub(
                     kind: SymbolKind::Text,
                     weak: false,
                     section: SymbolSection::Section(text_section),
-                    flags: object::SymbolFlags::None,
+                    flags: SymbolFlags::None, // ignore for these stubs
+                });
+            }
+
+            // Rust code typically generates Tls accessors as functions (text), but they are referenced
+            // indirectly as data symbols. We end up handling this by adding the TLS symbol as a data
+            // symbol with the initializer as the address of the original tls initializer. That way
+            // if new TLS are added at runtime, they get initialized properly, but otherwise, the
+            // tls initialization check (cbz) properly skips re-initialization on patches.
+            //
+            // ```
+            // __ZN17crossbeam_channel5waker17current_thread_id9THREAD_ID29_$u7b$$u7b$constant$u7d$$u7d$28_$u7b$$u7b$closure$u7d$$u7d$17h33618d877d86bb77E:
+            //    stp     x20, x19, [sp, #-0x20]!
+            //    stp     x29, x30, [sp, #0x10]
+            //    add     x29, sp, #0x10
+            //    adrp    x19, 21603 ; 0x1054bd000
+            //    add     x19, x19, #0x998
+            //    ldr     x20, [x19]
+            //    mov     x0, x19
+            //    blr     x20
+            //    ldr     x8, [x0]
+            //    cbz     x8, 0x10005acc0
+            //    mov     x0, x19
+            //    blr     x20
+            //    ldp     x29, x30, [sp, #0x10]
+            //    ldp     x20, x19, [sp], #0x20
+            //    ret
+            //    mov     x0, x19
+            //    blr     x20
+            //    bl      __ZN3std3sys12thread_local6native4lazy20Storage$LT$T$C$D$GT$10initialize17h818476638edff4e6E
+            //    b       0x10005acac
+            // ```
+            SymbolKind::Tls => {
+                let tls_section = obj.section_id(StandardSection::Tls);
+
+                let pointer_width = match triple.pointer_width().unwrap() {
+                    PointerWidth::U16 => 2,
+                    PointerWidth::U32 => 4,
+                    PointerWidth::U64 => 8,
+                };
+
+                let size = if sym.size == 0 {
+                    pointer_width
+                } else {
+                    sym.size
+                };
+
+                let align = size.min(pointer_width).next_power_of_two();
+                let mut init = vec![0u8; size as usize];
+
+                // write the contents of the symbol to the init vec
+                init.iter_mut()
+                    .zip(match triple.endianness() {
+                        Ok(target_lexicon::Endianness::Little) => abs_addr.to_le_bytes(),
+                        Ok(target_lexicon::Endianness::Big) => abs_addr.to_be_bytes(),
+                        _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())),
+                    })
+                    .for_each(|(b, v)| *b = v);
+
+                let offset = obj.append_section_data(tls_section, &init, align);
+
+                obj.add_symbol(Symbol {
+                    name: name.as_bytes()[name_offset..].to_vec(),
+                    value: offset, // offset inside .tdata
+                    size,
+                    scope: SymbolScope::Linkage,
+                    kind: SymbolKind::Tls,
+                    weak: false,
+                    section: SymbolSection::Section(tls_section),
+                    flags: SymbolFlags::None, // ignore for these stubs
                 });
             }
 
@@ -1036,6 +1129,15 @@ pub fn create_undefined_symbol_stub(
                     SymbolKind::Unknown => SymbolKind::Data,
                     k => k,
                 };
+
+                // plain linux *wants* these flags, but android doesn't.
+                // unsure what's going on here, but this is special cased for now.
+                // I think the more advanced linkers don't want these flags, but the default linux linker (ld) does.
+                let flags = match triple.environment {
+                    target_lexicon::Environment::Android => SymbolFlags::None,
+                    _ => sym.flags,
+                };
+
                 obj.add_symbol(Symbol {
                     name: name.as_bytes()[name_offset..].to_vec(),
                     value: abs_addr,
@@ -1044,7 +1146,7 @@ pub fn create_undefined_symbol_stub(
                     kind,
                     weak: sym.is_weak,
                     section: SymbolSection::Absolute,
-                    flags: object::SymbolFlags::None,
+                    flags,
                 });
             }
         }
@@ -1212,7 +1314,7 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
 ///
 /// Uses the heuristics from the wasm-bindgen source code itself:
 ///
-/// https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1165
+/// <https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1165>
 fn name_is_bindgen_symbol(name: &str) -> bool {
     name.contains("__wbindgen_describe")
         || name.contains("__wbindgen_externref")
@@ -1367,3 +1469,18 @@ fn parse_module_with_ids(bindgened: &[u8]) -> Result<ParsedModule> {
         symbols,
     })
 }
+
+/// Get the main sentinel symbol for the given target triple
+///
+/// We need to special case darwin since `main` is the entrypoint but `_main` is the actual symbol.
+/// The entrypoint ends up outside the text section, seemingly, and breaks our aslr detection.
+fn main_sentinel(triple: &Triple) -> &'static str {
+    match triple.operating_system {
+        // The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
+        OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
+            "_main"
+        }
+
+        _ => "main",
+    }
+}

+ 148 - 0
packages/cli/src/build/pre_render.rs

@@ -0,0 +1,148 @@
+use anyhow::Context;
+use dioxus_cli_config::{server_ip, server_port};
+use dioxus_dx_wire_format::BuildStage;
+use futures_util::{stream::FuturesUnordered, StreamExt};
+use std::{
+    net::{IpAddr, Ipv4Addr, SocketAddr},
+    time::Duration,
+};
+use tokio::process::Command;
+
+use crate::BuildId;
+
+use super::{AppBuilder, BuilderUpdate};
+
+/// Pre-render the static routes, performing static-site generation
+pub(crate) async fn pre_render_static_routes(
+    devserver_ip: Option<SocketAddr>,
+    builder: &mut AppBuilder,
+    updates: Option<&futures_channel::mpsc::UnboundedSender<BuilderUpdate>>,
+) -> anyhow::Result<()> {
+    if let Some(updates) = updates {
+        updates
+            .unbounded_send(BuilderUpdate::Progress {
+                stage: BuildStage::Prerendering,
+            })
+            .unwrap();
+    }
+    let server_exe = builder.build.main_exe();
+
+    // Use the address passed in through environment variables or default to localhost:9999. We need
+    // to default to a value that is different than the CLI default address to avoid conflicts
+    let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
+    let port = server_port().unwrap_or(9999);
+    let fullstack_address = SocketAddr::new(ip, port);
+    let address = fullstack_address.ip().to_string();
+    let port = fullstack_address.port().to_string();
+
+    // Borrow port and address so we can easily move them into multiple tasks below
+    let address = &address;
+    let port = &port;
+
+    tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
+
+    let vars = builder.child_environment_variables(
+        devserver_ip,
+        Some(fullstack_address),
+        false,
+        BuildId::SERVER,
+    );
+    // Run the server executable
+    let _child = Command::new(&server_exe)
+        .envs(vars)
+        .current_dir(server_exe.parent().unwrap())
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .kill_on_drop(true)
+        .spawn()?;
+
+    // Borrow reqwest_client so we only move the reference into the futures
+    let reqwest_client = reqwest::Client::new();
+    let reqwest_client = &reqwest_client;
+
+    // Get the routes from the `/static_routes` endpoint
+    let mut routes = None;
+
+    // The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
+    const RETRY_ATTEMPTS: usize = 5;
+    for i in 0..=RETRY_ATTEMPTS {
+        tracing::debug!(
+            "Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}"
+        );
+
+        let request = reqwest_client
+            .post(format!("http://{address}:{port}/api/static_routes"))
+            .body("{}".to_string())
+            .send()
+            .await;
+        match request {
+            Ok(request) => {
+                routes = Some(request
+                    .json::<Vec<String>>()
+                    .await
+                    .inspect(|text| tracing::debug!("Got static routes: {text:?}"))
+                    .context("Failed to parse static routes from the server. Make sure your server function returns Vec<String> with the (default) json encoding")?);
+                break;
+            }
+            Err(err) => {
+                // If the request fails, try  up to 5 times with a one second delay
+                // If it fails 5 times, return the error
+                if i == RETRY_ATTEMPTS {
+                    return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec<String> of static routes.");
+                }
+                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+            }
+        }
+    }
+
+    let routes = routes.expect(
+        "static routes should exist or an error should have been returned on the last attempt",
+    );
+
+    // Create a pool of futures that cache each route
+    let mut resolved_routes = routes
+        .into_iter()
+        .map(|route| async move {
+            tracing::info!("Rendering {route} for SSG");
+
+            // For each route, ping the server to force it to cache the response for ssg
+            let request = reqwest_client
+                .get(format!("http://{address}:{port}{route}"))
+                .header("Accept", "text/html")
+                .send()
+                .await?;
+
+            // If it takes longer than 30 seconds to resolve the route, log a warning
+            let warning_task = tokio::spawn({
+                let route = route.clone();
+                async move {
+                    tokio::time::sleep(Duration::from_secs(30)).await;
+                    tracing::warn!("Route {route} has been rendering for 30 seconds");
+                }
+            });
+
+            // Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
+            // because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
+            // the final clean HTML to the disk automatically after the request completes.
+            let _html = request.text().await?;
+
+            // Cancel the warning task if it hasn't already run
+            warning_task.abort();
+
+            Ok::<_, reqwest::Error>(route)
+        })
+        .collect::<FuturesUnordered<_>>();
+
+    while let Some(route) = resolved_routes.next().await {
+        match route {
+            Ok(route) => tracing::debug!("ssg success: {route:?}"),
+            Err(err) => tracing::error!("ssg error: {err:?}"),
+        }
+    }
+
+    tracing::info!("SSG complete");
+
+    drop(_child);
+
+    Ok(())
+}

+ 977 - 406
packages/cli/src/build/request.rs

@@ -55,7 +55,7 @@
 //!
 //! Currently, we defer most of our deploy-based bundling to Tauri bundle, though we should migrate
 //! to just bundling everything ourselves. This would require us to implement code-signing which
-//! is a bit of a pain, but fortunately a solved process (https://github.com/rust-mobile/xbuild).
+//! is a bit of a pain, but fortunately a solved process (<https://github.com/rust-mobile/xbuild>).
 //!
 //! ## Build Structure
 //!
@@ -107,7 +107,7 @@
 //!
 //! ### Linux:
 //!
-//! https://docs.appimage.org/reference/appdir.html#ref-appdir
+//! <https://docs.appimage.org/reference/appdir.html#ref-appdir>
 //! current_exe.join("Assets")
 //! ```
 //! app.appimage/
@@ -157,7 +157,7 @@
 //! 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
+//! <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.
@@ -309,27 +309,31 @@
 //! 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>
+//! - 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>
 
 use crate::{
-    AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, Platform, Result, RustcArgs,
-    TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
+    AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, LinkerFlavor, Platform, Result,
+    RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace,
+    DX_RUSTC_WRAPPER_ENV_VAR,
 };
 use anyhow::Context;
+use cargo_metadata::diagnostic::Diagnostic;
 use dioxus_cli_config::format_base_path_meta_element;
 use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
 use dioxus_cli_opt::{process_file_to, AssetManifest};
 use itertools::Itertools;
 use krates::{cm::TargetKind, NodeId};
-use manganis::{AssetOptions, JsAssetOptions};
+use manganis::AssetOptions;
+use manganis_core::AssetVariant;
 use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
 use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
 use std::{
-    collections::HashSet,
+    collections::{BTreeMap, HashSet},
     io::Write,
     path::{Path, PathBuf},
     process::Stdio,
@@ -342,7 +346,6 @@ use std::{
 use target_lexicon::{OperatingSystem, Triple};
 use tempfile::{NamedTempFile, TempDir};
 use tokio::{io::AsyncBufReadExt, process::Command};
-use toml_edit::Item;
 use uuid::Uuid;
 
 use super::HotpatchModuleCache;
@@ -370,13 +373,15 @@ pub(crate) struct BuildRequest {
     pub(crate) platform: Platform,
     pub(crate) enabled_platforms: Vec<Platform>,
     pub(crate) triple: Triple,
-    pub(crate) _device: bool,
+    pub(crate) device: bool,
     pub(crate) package: String,
+    pub(crate) main_target: String,
     pub(crate) features: Vec<String>,
+    pub(crate) rustflags: cargo_config2::Flags,
     pub(crate) extra_cargo_args: Vec<String>,
     pub(crate) extra_rustc_args: Vec<String>,
     pub(crate) no_default_features: bool,
-    pub(crate) custom_target_dir: Option<PathBuf>,
+    pub(crate) target_dir: PathBuf,
     pub(crate) skip_assets: bool,
     pub(crate) wasm_split: bool,
     pub(crate) debug_symbols: bool,
@@ -386,6 +391,8 @@ pub(crate) struct BuildRequest {
     pub(crate) link_args_file: Arc<NamedTempFile>,
     pub(crate) link_err_file: Arc<NamedTempFile>,
     pub(crate) rustc_wrapper_args_file: Arc<NamedTempFile>,
+    pub(crate) base_path: Option<String>,
+    pub(crate) using_dioxus_explicitly: bool,
 }
 
 /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin
@@ -438,10 +445,6 @@ pub struct BuildArtifacts {
     pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
 }
 
-pub(crate) static PROFILE_WASM: &str = "wasm-dev";
-pub(crate) static PROFILE_ANDROID: &str = "android-dev";
-pub(crate) static PROFILE_SERVER: &str = "server-dev";
-
 impl BuildRequest {
     /// Create a new build request.
     ///
@@ -460,7 +463,11 @@ impl BuildRequest {
     ///
     /// Note: Build requests are typically created only when the CLI is invoked or when significant
     /// changes are detected in the `Cargo.toml` (e.g., features added or removed).
-    pub(crate) async fn new(args: &TargetArgs, workspace: Arc<Workspace>) -> Result<Self> {
+    pub(crate) async fn new(
+        args: &TargetArgs,
+        main_target: Option<String>,
+        workspace: Arc<Workspace>,
+    ) -> Result<Self> {
         let crate_package = workspace.find_main_package(args.package.clone())?;
 
         let config = workspace
@@ -503,6 +510,10 @@ impl BuildRequest {
             })
             .unwrap_or(workspace.krates[crate_package].name.clone());
 
+        // Use the main_target for the client + server build if it is set, otherwise use the target name for this
+        // specific build
+        let main_target = main_target.unwrap_or(target_name.clone());
+
         let crate_target = main_package
             .targets
             .iter()
@@ -536,6 +547,10 @@ impl BuildRequest {
         // We collect all the platforms it enables first and then select based on the --platform arg
         let enabled_platforms =
             Self::enabled_cargo_toml_platforms(main_package, args.no_default_features);
+        let using_dioxus_explicitly = main_package
+            .dependencies
+            .iter()
+            .any(|dep| dep.name == "dioxus");
 
         let mut features = args.features.clone();
         let mut no_default_features = args.no_default_features;
@@ -552,6 +567,7 @@ impl BuildRequest {
                     platform
                 }
             },
+            None if !using_dioxus_explicitly => Platform::autodetect_from_cargo_feature("desktop").unwrap(),
             None => match enabled_platforms.len() {
                 0 => return Err(anyhow::anyhow!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`").into()),
                 1 => enabled_platforms[0],
@@ -565,20 +581,16 @@ impl BuildRequest {
         };
 
         // Add any features required to turn on the client
-        features.push(Self::feature_for_platform(main_package, platform));
+        if using_dioxus_explicitly {
+            features.push(Self::feature_for_platform(main_package, platform));
+        }
 
         // Set the profile of the build if it's not already set
         // This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles
         // We might want to move some of these profiles into dioxus.toml and make them "virtual".
         let profile = match args.profile.clone() {
             Some(profile) => profile,
-            None if args.release => "release".to_string(),
-            None => match platform {
-                Platform::Android => PROFILE_ANDROID.to_string(),
-                Platform::Web => PROFILE_WASM.to_string(),
-                Platform::Server => PROFILE_SERVER.to_string(),
-                _ => "dev".to_string(),
-            },
+            None => platform.profile_name(args.release),
         };
 
         // Determining release mode is based on the profile, actually, so we need to check that
@@ -594,7 +606,7 @@ impl BuildRequest {
         // We usually use the simulator unless --device is passed *or* a device is detected by probing.
         // For now, though, since we don't have probing, it just defaults to false
         // Tools like xcrun/adb can detect devices
-        let device = args.device.unwrap_or(false);
+        let device = args.device;
 
         // We want a real triple to build with, so we'll autodetect it if it's not provided
         // The triple ends up being a source of truth for us later hence all this work to figure it out
@@ -621,7 +633,7 @@ impl BuildRequest {
                         Architecture::Aarch64(_) if device => "aarch64-apple-ios".parse().unwrap(),
                         Architecture::Aarch64(_) => "aarch64-apple-ios-sim".parse().unwrap(),
                         _ if device => "x86_64-apple-ios".parse().unwrap(),
-                        _ => "x86_64-apple-ios-sim".parse().unwrap(),
+                        _ => "x86_64-apple-ios".parse().unwrap(),
                     }
                 }
 
@@ -635,11 +647,54 @@ impl BuildRequest {
             },
         };
 
-        let custom_linker = if platform == Platform::Android {
-            Some(workspace.android_tools()?.android_cc(&triple))
-        } else {
-            None
-        };
+        // Somethings we override are also present in the user's config.
+        // If we can't get them by introspecting cargo, then we need to get them from the config
+        //
+        // This involves specifically two fields:
+        // - The linker since we override it for Android and hotpatching
+        // - RUSTFLAGS since we also override it for Android and hotpatching
+        let cargo_config = cargo_config2::Config::load().unwrap();
+        let mut custom_linker = cargo_config.linker(triple.to_string()).ok().flatten();
+        let mut rustflags = cargo_config2::Flags::default();
+
+        if matches!(platform, Platform::Android) {
+            rustflags.flags.extend([
+                "-Clink-arg=-landroid".to_string(),
+                "-Clink-arg=-llog".to_string(),
+                "-Clink-arg=-lOpenSLES".to_string(),
+                "-Clink-arg=-Wl,--export-dynamic".to_string(),
+            ]);
+        }
+
+        // Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
+        for env in [
+            "RUSTFLAGS".to_string(),
+            format!("CARGO_TARGET_{triple}_RUSTFLAGS"),
+        ] {
+            if let Ok(flags) = std::env::var(env) {
+                rustflags
+                    .flags
+                    .extend(cargo_config2::Flags::from_space_separated(&flags).flags);
+            }
+        }
+
+        // Use the user's linker if the specify it at the target level
+        if let Ok(target) = cargo_config.target(triple.to_string()) {
+            if let Some(flags) = target.rustflags {
+                rustflags.flags.extend(flags.flags);
+            }
+        }
+
+        // If no custom linker is set, then android falls back to us as the linker
+        if custom_linker.is_none() && platform == Platform::Android {
+            custom_linker = Some(workspace.android_tools()?.android_cc(&triple));
+        }
+
+        let target_dir = std::env::var("CARGO_TARGET_DIR")
+            .ok()
+            .map(PathBuf::from)
+            .or_else(|| cargo_config.build.target_dir.clone())
+            .unwrap_or_else(|| workspace.workspace_root().join("target"));
 
         // Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!)
         let link_args_file = Arc::new(
@@ -666,14 +721,19 @@ impl BuildRequest {
 
         tracing::debug!(
             r#"Log Files:
-link_args_file: {},
-link_err_file: {},
-rustc_wrapper_args_file: {},
-session_cache_dir: {}"#,
+                • link_args_file: {},
+                • link_err_file: {},
+                • rustc_wrapper_args_file: {},
+                • session_cache_dir: {}
+                • linker: {:?}
+                • target_dir: {:?}
+                "#,
             link_args_file.path().display(),
             link_err_file.path().display(),
             rustc_wrapper_args_file.path().display(),
             session_cache_dir.path().display(),
+            custom_linker,
+            target_dir,
         );
 
         Ok(Self {
@@ -684,11 +744,11 @@ session_cache_dir: {}"#,
             crate_target,
             profile,
             triple,
-            _device: device,
+            device,
             workspace,
             config,
             enabled_platforms,
-            custom_target_dir: None,
+            target_dir,
             custom_linker,
             link_args_file,
             link_err_file,
@@ -698,7 +758,11 @@ session_cache_dir: {}"#,
             extra_cargo_args,
             release,
             package,
+            main_target,
+            rustflags,
+            using_dioxus_explicitly,
             skip_assets: args.skip_assets,
+            base_path: args.base_path.clone(),
             wasm_split: args.wasm_split,
             debug_symbols: args.debug_symbols,
             inject_loading_scripts: args.inject_loading_scripts,
@@ -718,9 +782,10 @@ session_cache_dir: {}"#,
             BuildMode::Thin {
                 aslr_reference,
                 cache,
+                rustc_args,
                 ..
             } => {
-                self.write_patch(ctx, *aslr_reference, &mut artifacts, cache)
+                self.write_patch(ctx, *aslr_reference, &mut artifacts, cache, rustc_args)
                     .await?;
             }
 
@@ -728,16 +793,12 @@ session_cache_dir: {}"#,
                 ctx.status_start_bundle();
 
                 self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
-                    .await
-                    .context("Failed to write main executable")?;
-                self.write_assets(ctx, &artifacts.assets)
-                    .await
-                    .context("Failed to write assets")?;
+                    .await?;
+                self.write_frameworks(ctx, &artifacts.direct_rustc).await?;
+                self.write_assets(ctx, &artifacts.assets).await?;
                 self.write_metadata().await?;
                 self.optimize(ctx).await?;
-                self.assemble(ctx)
-                    .await
-                    .context("Failed to assemble app bundle")?;
+                self.assemble(ctx).await?;
 
                 tracing::debug!("Bundle created at {}", self.root_dir().display());
             }
@@ -800,7 +861,7 @@ session_cache_dir: {}"#,
 
             match message {
                 Message::BuildScriptExecuted(_) => units_compiled += 1,
-                Message::CompilerMessage(msg) => ctx.status_build_diagnostic(msg),
+                Message::CompilerMessage(msg) => ctx.status_build_diagnostic(msg.message),
                 Message::TextLine(line) => {
                     // Handle the case where we're getting lines directly from rustc.
                     // These are in a different format than the normal cargo output, though I imagine
@@ -822,6 +883,11 @@ session_cache_dir: {}"#,
                         }
                     }
 
+                    // Handle direct rustc diagnostics
+                    if let Ok(diag) = serde_json::from_str::<Diagnostic>(&line) {
+                        ctx.status_build_diagnostic(diag);
+                    }
+
                     // For whatever reason, if there's an error while building, we still receive the TextLine
                     // instead of an "error" message. However, the following messages *also* tend to
                     // be the error message, and don't start with "error:". So we'll check if we've already
@@ -860,8 +926,6 @@ session_cache_dir: {}"#,
             }
         }
 
-        let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
-
         // Accumulate the rustc args from the wrapper, if they exist and can be parsed.
         let mut direct_rustc = RustcArgs::default();
         if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file.path()) {
@@ -873,20 +937,46 @@ session_cache_dir: {}"#,
         // If there's any warnings from the linker, we should print them out
         if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file.path()) {
             if !linker_warnings.is_empty() {
-                tracing::warn!("Linker warnings: {}", linker_warnings);
+                if output_location.is_none() {
+                    tracing::error!("Linker warnings: {}", linker_warnings);
+                } else {
+                    tracing::debug!("Linker warnings: {}", linker_warnings);
+                }
             }
         }
 
+        // Collect the linker args from the and update the rustc args
+        direct_rustc.link_args = std::fs::read_to_string(self.link_args_file.path())
+            .context("Failed to read link args from file")?
+            .lines()
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>();
+
+        let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
+
         // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds
         if matches!(ctx.mode, BuildMode::Fat) {
-            self.run_fat_link(ctx, &exe).await?;
+            let link_start = SystemTime::now();
+            self.run_fat_link(ctx, &exe, &direct_rustc).await?;
+            tracing::debug!(
+                "Fat linking completed in {}us",
+                SystemTime::now()
+                    .duration_since(link_start)
+                    .unwrap()
+                    .as_micros()
+            );
         }
 
         let assets = self.collect_assets(&exe, ctx)?;
         let time_end = SystemTime::now();
         let mode = ctx.mode.clone();
         let platform = self.platform;
-        tracing::debug!("Build completed successfully - output location: {:?}", exe);
+
+        tracing::debug!(
+            "Build completed successfully in {}us: {:?}",
+            time_end.duration_since(time_start).unwrap().as_micros(),
+            exe
+        );
 
         Ok(BuildArtifacts {
             time_end,
@@ -900,20 +990,16 @@ session_cache_dir: {}"#,
         })
     }
 
-    /// Traverse the target directory and collect all assets from the incremental cache
-    ///
-    /// This uses "known paths" that have stayed relatively stable during cargo's lifetime.
-    /// One day this system might break and we might need to go back to using the linker approach.
+    /// Collect the assets from the final executable and modify the binary in place to point to the right
+    /// hashed asset location.
     fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
-        tracing::debug!("Collecting assets from exe at {} ...", exe.display());
-
         // walk every file in the incremental cache dir, reading and inserting items into the manifest.
         let mut manifest = AssetManifest::default();
 
         // And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
         if !self.skip_assets {
             ctx.status_extracting_assets();
-            _ = manifest.add_from_object_path(exe);
+            manifest = super::assets::extract_assets_from_file(exe)?;
         }
 
         Ok(manifest)
@@ -978,12 +1064,6 @@ session_cache_dir: {}"#,
             | Platform::Ios
             | Platform::Liveview
             | Platform::Server => {
-                // We wipe away the dir completely, which is not great behavior :/
-                // Don't wipe server since web will need this folder too.
-                if self.platform != Platform::Server {
-                    _ = std::fs::remove_dir_all(self.exe_dir());
-                }
-
                 std::fs::create_dir_all(self.exe_dir())?;
                 std::fs::copy(exe, self.main_exe())?;
             }
@@ -992,6 +1072,54 @@ session_cache_dir: {}"#,
         Ok(())
     }
 
+    async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
+        let framework_dir = self.frameworks_folder();
+
+        for arg in &direct_rustc.link_args {
+            // todo - how do we handle windows dlls? we don't want to bundle the system dlls
+            // for now, we don't do anything with dlls, and only use .dylibs and .so files
+
+            if arg.ends_with(".dylib") | arg.ends_with(".so") {
+                let from = PathBuf::from(arg);
+                let to = framework_dir.join(from.file_name().unwrap());
+                _ = std::fs::remove_file(&to);
+
+                tracing::debug!("Copying framework from {from:?} to {to:?}");
+
+                _ = std::fs::create_dir_all(&framework_dir);
+
+                // in dev and on normal oses, we want to symlink the file
+                // otherwise, just copy it (since in release you want to distribute the framework)
+                if cfg!(any(windows, unix)) && !self.release {
+                    #[cfg(windows)]
+                    std::os::windows::fs::symlink_file(from, to).with_context(|| {
+                        "Failed to symlink framework into bundle: {from:?} -> {to:?}"
+                    })?;
+
+                    #[cfg(unix)]
+                    std::os::unix::fs::symlink(from, to).with_context(|| {
+                        "Failed to symlink framework into bundle: {from:?} -> {to:?}"
+                    })?;
+                } else {
+                    std::fs::copy(from, to)?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn frameworks_folder(&self) -> PathBuf {
+        match self.triple.operating_system {
+            OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => {
+                self.root_dir().join("Contents").join("Frameworks")
+            }
+            OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"),
+            OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(),
+            _ => self.root_dir(),
+        }
+    }
+
     /// 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
@@ -1001,6 +1129,14 @@ session_cache_dir: {}"#,
             return Ok(());
         }
 
+        // Run the tailwind build before bundling anything else
+        crate::TailwindCli::run_once(
+            self.package_manifest_dir(),
+            self.config.application.tailwind_input.clone(),
+            self.config.application.tailwind_output.clone(),
+        )
+        .await?;
+
         let asset_dir = self.asset_dir();
 
         // First, clear the asset dir of any files that don't exist in the new manifest
@@ -1008,8 +1144,7 @@ session_cache_dir: {}"#,
 
         // Create a set of all the paths that new files will be bundled to
         let mut keep_bundled_output_paths: HashSet<_> = assets
-            .assets
-            .values()
+            .assets()
             .map(|a| asset_dir.join(a.bundled_path()))
             .collect();
 
@@ -1048,8 +1183,8 @@ session_cache_dir: {}"#,
         let mut assets_to_transfer = vec![];
 
         // Queue the bundled assets
-        for (asset, bundled) in &assets.assets {
-            let from = asset.clone();
+        for bundled in assets.assets() {
+            let from = PathBuf::from(bundled.absolute_source_path());
             let to = asset_dir.join(bundled.bundled_path());
 
             // prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
@@ -1102,8 +1237,10 @@ session_cache_dir: {}"#,
         .await
         .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
 
-        // // Remove the wasm bindgen output directory if it exists
-        // _ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir());
+        // Remove the wasm dir if we packaged it to an "asset"-type app
+        if self.should_bundle_to_asset() {
+            _ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir());
+        }
 
         // Write the version file so we know what version of the optimizer we used
         std::fs::write(self.asset_optimizer_version_file(), crate::VERSION.as_str())?;
@@ -1123,6 +1260,7 @@ session_cache_dir: {}"#,
         aslr_reference: u64,
         artifacts: &mut BuildArtifacts,
         cache: &Arc<HotpatchModuleCache>,
+        rustc_args: &RustcArgs,
     ) -> Result<()> {
         ctx.status_hotpatching();
 
@@ -1151,7 +1289,7 @@ session_cache_dir: {}"#,
         //
         // Many args are passed twice, too, which can be confusing, but generally don't have any real
         // effect. Note that on macos/ios, there's a special macho header that needs to be set, otherwise
-        // dyld will complain.a
+        // dyld will complain.
         //
         // Also, some flags in darwin land might become deprecated, need to be super conservative:
         // - https://developer.apple.com/forums/thread/773907
@@ -1186,6 +1324,7 @@ session_cache_dir: {}"#,
         //     -nodefaultlibs
         //     -Wl,-all_load
         // ```
+        let mut dylibs = vec![];
         let mut object_files = args
             .iter()
             .filter(|arg| arg.ends_with(".rcgu.o"))
@@ -1219,11 +1358,19 @@ session_cache_dir: {}"#,
             let patch_file = self.main_exe().with_file_name("stub.o");
             std::fs::write(&patch_file, stub_bytes)?;
             object_files.push(patch_file);
+
+            // Add the dylibs/sos to the linker args
+            // Make sure to use the one in the bundle, not the ones in the target dir or system.
+            for arg in &rustc_args.link_args {
+                if arg.ends_with(".dylib") || arg.ends_with(".so") {
+                    let path = PathBuf::from(arg);
+                    dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
+                }
+            }
         }
 
         // And now we can run the linker with our new args
         let linker = self.select_linker()?;
-
         let out_exe = self.patch_exe(artifacts.time_start);
         let out_arg = match self.triple.operating_system {
             OperatingSystem::Windows => vec![format!("/OUT:{}", out_exe.display())],
@@ -1238,8 +1385,11 @@ session_cache_dir: {}"#,
         // does it since it uses llvm-objcopy into the `target/debug/` folder.
         let res = Command::new(linker)
             .args(object_files.iter())
+            .args(dylibs.iter())
             .args(self.thin_link_args(&args)?)
             .args(out_arg)
+            .env_clear()
+            .envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
             .output()
             .await?;
 
@@ -1265,9 +1415,11 @@ session_cache_dir: {}"#,
         }
 
         // Now extract the assets from the fat binary
-        artifacts
-            .assets
-            .add_from_object_path(&self.patch_exe(artifacts.time_start))?;
+        artifacts.assets = self.collect_assets(&self.patch_exe(artifacts.time_start), ctx)?;
+
+        // If this is a web build, reset the index.html file in case it was modified by SSG
+        self.write_index_html(&artifacts.assets)
+            .context("Failed to write index.html")?;
 
         // Clean up the temps manually
         // todo: we might want to keep them around for debugging purposes
@@ -1283,12 +1435,9 @@ session_cache_dir: {}"#,
     /// This is basically just stripping away the rlibs and other libraries that will be satisfied
     /// by our stub step.
     fn thin_link_args(&self, original_args: &[&str]) -> Result<Vec<String>> {
-        use target_lexicon::OperatingSystem;
-
-        let triple = self.triple.clone();
         let mut out_args = vec![];
 
-        match triple.operating_system {
+        match self.linker_flavor() {
             // wasm32-unknown-unknown -> use wasm-ld (gnu-lld)
             //
             // We need to import a few things - namely the memory and ifunc table.
@@ -1301,7 +1450,14 @@ session_cache_dir: {}"#,
             //
             // We don't use *any* of the original linker args since they do lots of custom exports
             // and other things that we don't need.
-            OperatingSystem::Unknown if self.platform == Platform::Web => {
+            //
+            // The trickiest one here is -Crelocation-model=pic, which forces data symbols
+            // into a GOT, making it possible to import them from the main module.
+            //
+            // I think we can make relocation-model=pic work for non-wasm platforms, enabling
+            // fully relocatable modules with no host coordination in lieu of sending out
+            // the aslr slide at runtime.
+            LinkerFlavor::WasmLld => {
                 out_args.extend([
                     "--fatal-warnings".to_string(),
                     "--verbose".to_string(),
@@ -1316,13 +1472,21 @@ session_cache_dir: {}"#,
                     "--pie".to_string(),
                     "--experimental-pic".to_string(),
                 ]);
+
+                // retain exports so post-processing has hooks to work with
+                for (idx, arg) in original_args.iter().enumerate() {
+                    if *arg == "--export" {
+                        out_args.push(arg.to_string());
+                        out_args.push(original_args[idx + 1].to_string());
+                    }
+                }
             }
 
             // This uses "cc" and these args need to be ld compatible
             //
             // Most importantly, we want to pass `-dylib` to both CC and the linker to indicate that
             // we want to generate the shared library instead of an executable.
-            OperatingSystem::IOS(_) | OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) => {
+            LinkerFlavor::Darwin => {
                 out_args.extend(["-Wl,-dylib".to_string()]);
 
                 // Preserve the original args. We only preserve:
@@ -1345,7 +1509,7 @@ session_cache_dir: {}"#,
             // android/linux need to be compatible with lld
             //
             // android currently drags along its own libraries and other zany flags
-            OperatingSystem::Linux => {
+            LinkerFlavor::Gnu => {
                 out_args.extend([
                     "-shared".to_string(),
                     "-Wl,--eh-frame-hdr".to_string(),
@@ -1369,13 +1533,16 @@ session_cache_dir: {}"#,
                     if arg.starts_with("-l")
                         || arg.starts_with("-m")
                         || arg.starts_with("-Wl,--target=")
+                        || arg.starts_with("-Wl,-fuse-ld")
+                        || arg.starts_with("-fuse-ld")
+                        || arg.contains("-ld-path")
                     {
                         out_args.push(arg.to_string());
                     }
                 }
             }
 
-            OperatingSystem::Windows => {
+            LinkerFlavor::Msvc => {
                 out_args.extend([
                     "shlwapi.lib".to_string(),
                     "kernel32.lib".to_string(),
@@ -1390,11 +1557,12 @@ session_cache_dir: {}"#,
                     "/PDBALTPATH:%_PDB%".to_string(),
                     "/EXPORT:main".to_string(),
                     "/HIGHENTROPYVA:NO".to_string(),
-                    // "/SUBSYSTEM:WINDOWS".to_string(),
                 ]);
             }
 
-            _ => return Err(anyhow::anyhow!("Unsupported platform for thin linking").into()),
+            LinkerFlavor::Unsupported => {
+                return Err(anyhow::anyhow!("Unsupported platform for thin linking").into())
+            }
         }
 
         let extract_value = |arg: &str| -> Option<String> {
@@ -1439,15 +1607,12 @@ session_cache_dir: {}"#,
                 .unwrap_or(0),
         ));
 
-        let extension = match self.triple.operating_system {
-            OperatingSystem::Darwin(_) => "dylib",
-            OperatingSystem::MacOSX(_) => "dylib",
-            OperatingSystem::IOS(_) => "dylib",
-            OperatingSystem::Windows => "dll",
-            OperatingSystem::Linux => "so",
-            OperatingSystem::Wasi => "wasm",
-            OperatingSystem::Unknown if self.platform == Platform::Web => "wasm",
-            _ => "",
+        let extension = match self.linker_flavor() {
+            LinkerFlavor::Darwin => "dylib",
+            LinkerFlavor::Gnu => "so",
+            LinkerFlavor::WasmLld => "wasm",
+            LinkerFlavor::Msvc => "dll",
+            LinkerFlavor::Unsupported => "",
         };
 
         path.with_extension(extension)
@@ -1483,41 +1648,73 @@ session_cache_dir: {}"#,
     ///
     /// todo: I think we can traverse our immediate dependencies and inspect their symbols, unless they `pub use` a crate
     /// todo: we should try and make this faster with memmapping
-    pub(crate) async fn run_fat_link(&self, ctx: &BuildContext, exe: &Path) -> Result<()> {
+    pub(crate) async fn run_fat_link(
+        &self,
+        ctx: &BuildContext,
+        exe: &Path,
+        rustc_args: &RustcArgs,
+    ) -> Result<()> {
         ctx.status_starting_link();
 
-        let raw_args = std::fs::read_to_string(self.link_args_file.path())
-            .context("Failed to read link args from file")?;
-        let args = raw_args.lines().collect::<Vec<_>>();
-
         // Filter out the rlib files from the arguments
-        let rlibs = args
+        let rlibs = rustc_args
+            .link_args
             .iter()
             .filter(|arg| arg.ends_with(".rlib"))
             .map(PathBuf::from)
             .collect::<Vec<_>>();
 
-        // Acquire a hash from the rlib names
+        // Acquire a hash from the rlib names, sizes, modified times, and dx's git commit hash
+        // This ensures that any changes in dx or the rlibs will cause a new hash to be generated
+        // The hash relies on both dx and rustc hashes, so it should be thoroughly unique. Keep it
+        // short to avoid long file names.
         let hash_id = Uuid::new_v5(
             &Uuid::NAMESPACE_OID,
             rlibs
                 .iter()
-                .map(|p| p.file_name().unwrap().to_string_lossy())
+                .map(|p| {
+                    format!(
+                        "{}-{}-{}-{}",
+                        p.file_name().unwrap().to_string_lossy(),
+                        p.metadata().map(|m| m.len()).unwrap_or_default(),
+                        p.metadata()
+                            .ok()
+                            .and_then(|m| m.modified().ok())
+                            .and_then(|f| f.duration_since(UNIX_EPOCH).map(|f| f.as_secs()).ok())
+                            .unwrap_or_default(),
+                        crate::dx_build_info::GIT_COMMIT_HASH.unwrap_or_default()
+                    )
+                })
                 .collect::<String>()
                 .as_bytes(),
-        );
+        )
+        .to_string()
+        .chars()
+        .take(8)
+        .collect::<String>();
 
         // Check if we already have a cached object file
-        let out_ar_path = exe.with_file_name(format!("libfatdependencies-{hash_id}.a"));
+        let out_ar_path = exe.with_file_name(format!("libdeps-{hash_id}.a",));
+        let out_rlibs_list = exe.with_file_name(format!("rlibs-{hash_id}.txt"));
+        let mut archive_has_contents = out_ar_path.exists();
 
-        let mut compiler_rlibs = vec![];
+        // Use the rlibs list if it exists
+        let mut compiler_rlibs = std::fs::read_to_string(&out_rlibs_list)
+            .ok()
+            .map(|s| s.lines().map(PathBuf::from).collect::<Vec<_>>())
+            .unwrap_or_default();
 
         // Create it by dumping all the rlibs into it
         // This will include the std rlibs too, which can severely bloat the size of the archive
         //
         // The nature of this process involves making extremely fat archives, so we should try and
         // speed up the future linking process by caching the archive.
-        if !crate::devcfg::should_cache_dep_lib(&out_ar_path) {
+        //
+        // Since we're using the git hash for the CLI entropy, debug builds should always regenerate
+        // the archive since their hash might not change, but the logic might.
+        if !archive_has_contents || cfg!(debug_assertions) {
+            compiler_rlibs.clear();
+
             let mut bytes = vec![];
             let mut out_ar = ar::Builder::new(&mut bytes);
             for rlib in &rlibs {
@@ -1536,6 +1733,7 @@ session_cache_dir: {}"#,
 
                 let rlib_contents = std::fs::read(rlib)?;
                 let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
+                let mut keep_linker_rlib = false;
                 while let Some(Ok(object_file)) = reader.next_entry() {
                     let name = std::str::from_utf8(object_file.header().identifier()).unwrap();
                     if name.ends_with(".rmeta") {
@@ -1547,24 +1745,44 @@ session_cache_dir: {}"#,
                     }
 
                     // rlibs might contain dlls/sos/lib files which we don't want to include
-                    if name.ends_with(".dll") || name.ends_with(".so") || name.ends_with(".lib") {
-                        compiler_rlibs.push(rlib.to_owned());
+                    //
+                    // This catches .dylib, .so, .dll, .lib, .o, etc files that are not compatible with
+                    // our "fat archive" linking process.
+                    //
+                    // We only trust `.rcgu.o` files to make it into the --all_load archive.
+                    // This is a temporary stopgap to prevent issues with libraries that generate
+                    // object files that are not compatible with --all_load.
+                    // see https://github.com/DioxusLabs/dioxus/issues/4237
+                    if !(name.ends_with(".rcgu.o") || name.ends_with(".obj")) {
+                        keep_linker_rlib = true;
                         continue;
                     }
 
-                    if !(name.ends_with(".o") || name.ends_with(".obj")) {
-                        tracing::debug!("Unknown object file in rlib: {:?}", name);
-                    }
-
+                    archive_has_contents = true;
                     out_ar
                         .append(&object_file.header().clone(), object_file)
                         .context("Failed to add object file to archive")?;
                 }
+
+                // Some rlibs contain weird artifacts that we don't want to include in the fat archive.
+                // However, we still want them around in the linker in case the regular linker can handle them.
+                if keep_linker_rlib {
+                    compiler_rlibs.push(rlib.clone());
+                }
             }
 
             let bytes = out_ar.into_inner().context("Failed to finalize archive")?;
             std::fs::write(&out_ar_path, bytes).context("Failed to write archive")?;
             tracing::debug!("Wrote fat archive to {:?}", out_ar_path);
+
+            // Run the ranlib command to index the archive. This slows down this process a bit,
+            // but is necessary for some linkers to work properly.
+            // We ignore its error in case it doesn't recognize the architecture
+            if self.linker_flavor() == LinkerFlavor::Darwin {
+                if let Some(ranlib) = self.select_ranlib() {
+                    _ = Command::new(ranlib).arg(&out_ar_path).output().await;
+                }
+            }
         }
 
         compiler_rlibs.dedup();
@@ -1573,62 +1791,70 @@ session_cache_dir: {}"#,
         // And then remove the rest of the rlibs
         //
         // We also need to insert the -force_load flag to force the linker to load the archive
-        let mut args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
-        if let Some(first_rlib) = args.iter().position(|arg| arg.ends_with(".rlib")) {
-            match self.triple.operating_system {
-                OperatingSystem::Unknown if self.platform == Platform::Web => {
-                    // We need to use the --whole-archive flag for wasm
-                    args[first_rlib] = "--whole-archive".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.insert(first_rlib + 2, "--no-whole-archive".to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 3, rlib.display().to_string());
+        let mut args = rustc_args.link_args.clone();
+        if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) {
+            if archive_has_contents {
+                match self.linker_flavor() {
+                    LinkerFlavor::WasmLld => {
+                        args.insert(last_object, "--whole-archive".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.insert(last_object + 2, "--no-whole-archive".to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 3, rlib.display().to_string());
+                        }
                     }
-                }
-
-                // Subtle difference - on linux and android we go through clang and thus pass `-Wl,` prefix
-                OperatingSystem::Linux => {
-                    args[first_rlib] = "-Wl,--whole-archive".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.insert(first_rlib + 2, "-Wl,--no-whole-archive".to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 3, rlib.display().to_string());
+                    LinkerFlavor::Gnu => {
+                        args.insert(last_object, "-Wl,--whole-archive".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.insert(last_object + 2, "-Wl,--no-whole-archive".to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 3, rlib.display().to_string());
+                        }
                     }
-                }
-
-                OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
-                    args[first_rlib] = "-Wl,-force_load".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 2, rlib.display().to_string());
+                    LinkerFlavor::Darwin => {
+                        args.insert(last_object, "-Wl,-force_load".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 2, rlib.display().to_string());
+                        }
                     }
-
-                    args.insert(first_rlib + 3, "-Wl,-all_load".to_string());
-                }
-
-                OperatingSystem::Windows => {
-                    args[first_rlib] = format!("/WHOLEARCHIVE:{}", out_ar_path.display());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 1, rlib.display().to_string());
+                    LinkerFlavor::Msvc => {
+                        args.insert(
+                            last_object,
+                            format!("/WHOLEARCHIVE:{}", out_ar_path.display()),
+                        );
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 1, rlib.display().to_string());
+                        }
                     }
+                    LinkerFlavor::Unsupported => {
+                        tracing::error!("Unsupported platform for fat linking");
+                    }
+                };
+            }
+        }
 
-                    args.insert(first_rlib, "/HIGHENTROPYVA:NO".to_string());
-                }
+        // Add custom args to the linkers
+        match self.linker_flavor() {
+            LinkerFlavor::Gnu => {
+                // Export `main` so subsecond can use it for a reference point
+                args.push("-Wl,--export-dynamic-symbol,main".to_string());
+            }
+            LinkerFlavor::Darwin => {
+                args.push("-Wl,-exported_symbol,_main".to_string());
+            }
+            LinkerFlavor::Msvc => {
+                // Prevent alsr from overflowing 32 bits
+                args.push("/HIGHENTROPYVA:NO".to_string());
 
-                _ => {}
-            };
+                // Export `main` so subsecond can use it for a reference point
+                args.push("/EXPORT:main".to_string());
+            }
+            LinkerFlavor::WasmLld | LinkerFlavor::Unsupported => {}
         }
 
         // We also need to remove the `-o` flag since we want the linker output to end up in the
@@ -1657,6 +1883,7 @@ session_cache_dir: {}"#,
         let linker = self.select_linker()?;
 
         tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args);
+        tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs);
 
         // Run the linker directly!
         let out_arg = match self.triple.operating_system {
@@ -1667,6 +1894,8 @@ session_cache_dir: {}"#,
         let res = Command::new(linker)
             .args(args.iter().skip(1))
             .args(out_arg)
+            .env_clear()
+            .envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
             .output()
             .await?;
 
@@ -1689,9 +1918,64 @@ session_cache_dir: {}"#,
             _ = std::fs::remove_file(f);
         }
 
+        // Cache the rlibs list
+        _ = std::fs::write(
+            &out_rlibs_list,
+            compiler_rlibs
+                .into_iter()
+                .map(|s| s.display().to_string())
+                .join("\n"),
+        );
+
         Ok(())
     }
 
+    /// Automatically detect the linker flavor based on the target triple and any custom linkers.
+    ///
+    /// This tries to replicate what rustc does when selecting the linker flavor based on the linker
+    /// and triple.
+    fn linker_flavor(&self) -> LinkerFlavor {
+        if let Some(custom) = self.custom_linker.as_ref() {
+            let name = custom.file_name().unwrap().to_ascii_lowercase();
+            match name.to_str() {
+                Some("lld-link") => return LinkerFlavor::Msvc,
+                Some("lld-link.exe") => return LinkerFlavor::Msvc,
+                Some("wasm-ld") => return LinkerFlavor::WasmLld,
+                Some("ld64.lld") => return LinkerFlavor::Darwin,
+                Some("ld.lld") => return LinkerFlavor::Gnu,
+                Some("ld.gold") => return LinkerFlavor::Gnu,
+                Some("mold") => return LinkerFlavor::Gnu,
+                Some("sold") => return LinkerFlavor::Gnu,
+                Some("wild") => return LinkerFlavor::Gnu,
+                _ => {}
+            }
+        }
+
+        match self.triple.environment {
+            target_lexicon::Environment::Gnu
+            | target_lexicon::Environment::Gnuabi64
+            | target_lexicon::Environment::Gnueabi
+            | target_lexicon::Environment::Gnueabihf
+            | target_lexicon::Environment::GnuLlvm => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Musl => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Android => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Msvc => LinkerFlavor::Msvc,
+            target_lexicon::Environment::Macabi => LinkerFlavor::Darwin,
+            _ => match self.triple.operating_system {
+                OperatingSystem::Darwin(_) => LinkerFlavor::Darwin,
+                OperatingSystem::IOS(_) => LinkerFlavor::Darwin,
+                OperatingSystem::MacOSX(_) => LinkerFlavor::Darwin,
+                OperatingSystem::Linux => LinkerFlavor::Gnu,
+                OperatingSystem::Windows => LinkerFlavor::Msvc,
+                _ => match self.triple.architecture {
+                    target_lexicon::Architecture::Wasm32 => LinkerFlavor::WasmLld,
+                    target_lexicon::Architecture::Wasm64 => LinkerFlavor::WasmLld,
+                    _ => LinkerFlavor::Unsupported,
+                },
+            },
+        }
+    }
+
     /// Select the linker to use for this platform.
     ///
     /// We prefer to use the rust-lld linker when we can since it's usually there.
@@ -1701,25 +1985,25 @@ session_cache_dir: {}"#,
     /// cause issues with a custom linker setup. In theory, rust translates most flags to the right
     /// linker format.
     fn select_linker(&self) -> Result<PathBuf, Error> {
-        let cc = match self.triple.operating_system {
-            OperatingSystem::Unknown if self.platform == Platform::Web => self.workspace.wasm_ld(),
+        if let Some(linker) = self.custom_linker.clone() {
+            return Ok(linker);
+        }
 
-            // The android clang linker is *special* and has some android-specific flags that we need
-            //
-            // Note that this is *clang*, not `lld`.
-            OperatingSystem::Linux if self.platform == Platform::Android => {
-                self.workspace.android_tools()?.android_cc(&self.triple)
-            }
+        let cc = match self.linker_flavor() {
+            LinkerFlavor::WasmLld => self.workspace.wasm_ld(),
 
             // On macOS, we use the system linker since it's usually there.
             // We could also use `lld` here, but it might not be installed by default.
             //
             // Note that this is *clang*, not `lld`.
-            OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => self.workspace.cc(),
+            LinkerFlavor::Darwin => self.workspace.cc(),
+
+            // On Linux, we use the system linker since it's usually there.
+            LinkerFlavor::Gnu => self.workspace.cc(),
 
             // On windows, instead of trying to find the system linker, we just go with the lld.link
             // that rustup provides. It's faster and more stable then reyling on link.exe in path.
-            OperatingSystem::Windows => self.workspace.lld_link(),
+            LinkerFlavor::Msvc => self.workspace.lld_link(),
 
             // The rest of the platforms use `cc` as the linker which should be available in your path,
             // provided you have build-tools setup. On mac/linux this is the default, but on Windows
@@ -1733,7 +2017,7 @@ session_cache_dir: {}"#,
             // Note that "cc" is *not* a linker. It's a compiler! The arguments we pass need to be in
             // the form of `-Wl,<args>` for them to make it to the linker. This matches how rust does it
             // which is confusing.
-            _ => self.workspace.cc(),
+            LinkerFlavor::Unsupported => self.workspace.cc(),
         };
 
         Ok(cc)
@@ -1764,18 +2048,25 @@ session_cache_dir: {}"#,
                 cmd.current_dir(self.workspace_dir());
                 cmd.env_clear();
                 cmd.args(rustc_args.args[1..].iter());
-                cmd.envs(rustc_args.envs.iter().cloned());
                 cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
                 cmd.env_remove("RUSTC_WRAPPER");
                 cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR);
-                cmd.envs(self.cargo_build_env_vars(ctx)?);
+                cmd.envs(
+                    self.cargo_build_env_vars(ctx)?
+                        .iter()
+                        .map(|(k, v)| (k.as_ref(), v)),
+                );
                 cmd.arg(format!("-Clinker={}", Workspace::path_to_dx()?.display()));
 
                 if self.platform == Platform::Web {
                     cmd.arg("-Crelocation-model=pic");
                 }
 
-                tracing::trace!("Direct rustc command: {:#?}", rustc_args.args);
+                tracing::debug!("Direct rustc: {:#?}", cmd);
+
+                cmd.envs(rustc_args.envs.iter().cloned());
+
+                // tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
 
                 Ok(cmd)
             }
@@ -1798,9 +2089,11 @@ session_cache_dir: {}"#,
                     .arg("--message-format")
                     .arg("json-diagnostic-rendered-ansi")
                     .args(self.cargo_build_arguments(ctx))
-                    .envs(self.cargo_build_env_vars(ctx)?);
-
-                tracing::trace!("Cargo command: {:#?}", cmd);
+                    .envs(
+                        self.cargo_build_env_vars(ctx)?
+                            .iter()
+                            .map(|(k, v)| (k.as_ref(), v)),
+                    );
 
                 if ctx.mode == BuildMode::Fat {
                     cmd.env(
@@ -1816,6 +2109,8 @@ session_cache_dir: {}"#,
                     );
                 }
 
+                tracing::debug!("Cargo: {:#?}", cmd);
+
                 Ok(cmd)
             }
         }
@@ -1829,6 +2124,9 @@ session_cache_dir: {}"#,
     fn cargo_build_arguments(&self, ctx: &BuildContext) -> Vec<String> {
         let mut cargo_args = Vec::with_capacity(4);
 
+        // Set the `--config profile.{profile}.{key}={value}` flags for the profile, filling in adhoc profile
+        cargo_args.extend(self.profile_args());
+
         // Add required profile flags. --release overrides any custom profiles.
         cargo_args.push("--profile".to_string());
         cargo_args.push(self.profile.to_string());
@@ -1862,6 +2160,18 @@ session_cache_dir: {}"#,
         };
         cargo_args.push(self.executable_name().to_string());
 
+        // Set offline/locked/frozen
+        let lock_opts = crate::VERBOSITY.get().cloned().unwrap_or_default();
+        if lock_opts.frozen {
+            cargo_args.push("--frozen".to_string());
+        }
+        if lock_opts.locked {
+            cargo_args.push("--locked".to_string());
+        }
+        if lock_opts.offline {
+            cargo_args.push("--offline".to_string());
+        }
+
         // Merge in extra args. Order shouldn't really matter.
         cargo_args.extend(self.extra_cargo_args.clone());
         cargo_args.push("--".to_string());
@@ -1883,6 +2193,27 @@ session_cache_dir: {}"#,
             ));
         }
 
+        // for debuggability, we need to make sure android studio can properly understand our build
+        // https://stackoverflow.com/questions/68481401/debugging-a-prebuilt-shared-library-in-android-studio
+        if self.platform == Platform::Android {
+            cargo_args.push("-Clink-arg=-Wl,--build-id=sha1".to_string());
+        }
+
+        // Handle frameworks/dylibs by setting the rpath
+        // This is dependent on the bundle structure - in this case, appimage and appbundle for mac/linux
+        // todo: we need to figure out what to do for windows
+        match self.triple.operating_system {
+            OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
+                cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks".to_string());
+                cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string());
+            }
+            OperatingSystem::Linux => {
+                cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string());
+                cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string());
+            }
+            _ => {}
+        }
+
         // Our fancy hot-patching engine needs a lot of customization to work properly.
         //
         // These args are mostly intended to be passed when *fat* linking but are generally fine to
@@ -1942,14 +2273,7 @@ session_cache_dir: {}"#,
             //
             // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
             //
-            // The trickiest one here is -Crelocation-model=pic, which forces data symbols
-            // into a GOT, making it possible to import them from the main module.
-            //
-            // I think we can make relocation-model=pic work for non-wasm platforms, enabling
-            // fully relocatable modules with no host coordination in lieu of sending out
-            // the aslr slide at runtime.
-            //
-            // The other tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref
+            // The tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref
             // entries.
             //
             // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals
@@ -1958,7 +2282,6 @@ session_cache_dir: {}"#,
             if self.platform == Platform::Web
                 || self.triple.operating_system == OperatingSystem::Wasi
             {
-                // cargo_args.push("-Crelocation-model=pic".into());
                 cargo_args.push("-Ctarget-cpu=mvp".into());
                 cargo_args.push("-Clink-arg=--no-gc-sections".into());
                 cargo_args.push("-Clink-arg=--growable-table".into());
@@ -1974,7 +2297,7 @@ session_cache_dir: {}"#,
         cargo_args
     }
 
-    fn cargo_build_env_vars(&self, ctx: &BuildContext) -> Result<Vec<(&'static str, String)>> {
+    fn cargo_build_env_vars(&self, ctx: &BuildContext) -> Result<Vec<(Cow<'static, str>, String)>> {
         let mut env_vars = vec![];
 
         // Make sure to set all the crazy android flags. Cross-compiling is hard, man.
@@ -1982,49 +2305,54 @@ session_cache_dir: {}"#,
             env_vars.extend(self.android_env_vars()?);
         };
 
-        // If we're either zero-linking or using a custom linker, make `dx` itself do the linking.
-        if self.custom_linker.is_some()
-            || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
-        {
-            LinkAction {
-                triple: self.triple.clone(),
-                linker: self.custom_linker.clone(),
-                link_err_file: dunce::canonicalize(self.link_err_file.path())?,
-                link_args_file: dunce::canonicalize(self.link_args_file.path())?,
+        // If this is a release build, bake the base path and title into the binary with env vars.
+        // todo: should we even be doing this? might be better being a build.rs or something else.
+        if self.release {
+            if let Some(base_path) = self.base_path() {
+                env_vars.push((ASSET_ROOT_ENV.into(), base_path.to_string()));
             }
-            .write_env_vars(&mut env_vars)?;
+            env_vars.push((APP_TITLE_ENV.into(), self.config.web.app.title.clone()));
         }
 
+        // Assemble the rustflags by peering into the `.cargo/config.toml` file
+        let mut rust_flags = self.rustflags.clone();
+
         // Disable reference types on wasm when using hotpatching
         // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals
         if self.platform == Platform::Web
             && matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
         {
-            env_vars.push(("RUSTFLAGS", {
-                let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default();
-                rust_flags.push_str(" -Ctarget-cpu=mvp");
-                rust_flags
-            }));
+            rust_flags.flags.push("-Ctarget-cpu=mvp".to_string());
         }
 
-        if let Some(target_dir) = self.custom_target_dir.as_ref() {
-            env_vars.push(("CARGO_TARGET_DIR", target_dir.display().to_string()));
+        // Set the rust flags for the build if they're not empty.
+        if !rust_flags.flags.is_empty() {
+            env_vars.push((
+                "RUSTFLAGS".into(),
+                rust_flags
+                    .encode_space_separated()
+                    .context("Failed to encode RUSTFLAGS")?,
+            ));
         }
 
-        // If this is a release build, bake the base path and title into the binary with env vars.
-        // todo: should we even be doing this? might be better being a build.rs or something else.
-        if self.release {
-            if let Some(base_path) = self.base_path() {
-                env_vars.push((ASSET_ROOT_ENV, base_path.to_string()));
+        // If we're either zero-linking or using a custom linker, make `dx` itself do the linking.
+        if self.custom_linker.is_some()
+            || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
+        {
+            LinkAction {
+                triple: self.triple.clone(),
+                linker: self.custom_linker.clone(),
+                link_err_file: dunce::canonicalize(self.link_err_file.path())?,
+                link_args_file: dunce::canonicalize(self.link_args_file.path())?,
             }
-            env_vars.push((APP_TITLE_ENV, self.config.web.app.title.clone()));
+            .write_env_vars(&mut env_vars)?;
         }
 
         Ok(env_vars)
     }
 
-    fn android_env_vars(&self) -> Result<Vec<(&'static str, String)>> {
-        let mut env_vars = vec![];
+    fn android_env_vars(&self) -> Result<Vec<(Cow<'static, str>, String)>> {
+        let mut env_vars: Vec<(Cow<'static, str>, String)> = vec![];
 
         let tools = self.workspace.android_tools()?;
         let linker = tools.android_cc(&self.triple);
@@ -2044,38 +2372,42 @@ session_cache_dir: {}"#,
             java_home: {java_home:?}
             "#
         );
-        env_vars.push(("ANDROID_NATIVE_API_LEVEL", min_sdk_version.to_string()));
-        env_vars.push(("TARGET_AR", ar_path.display().to_string()));
-        env_vars.push(("TARGET_CC", target_cc.display().to_string()));
-        env_vars.push(("TARGET_CXX", target_cxx.display().to_string()));
-        env_vars.push(("ANDROID_NDK_ROOT", ndk.display().to_string()));
+        env_vars.push((
+            "ANDROID_NATIVE_API_LEVEL".into(),
+            min_sdk_version.to_string(),
+        ));
+        env_vars.push(("TARGET_AR".into(), ar_path.display().to_string()));
+        env_vars.push(("TARGET_CC".into(), target_cc.display().to_string()));
+        env_vars.push(("TARGET_CXX".into(), target_cxx.display().to_string()));
+        env_vars.push((
+            format!(
+                "CARGO_TARGET_{}_LINKER",
+                self.triple
+                    .to_string()
+                    .to_ascii_uppercase()
+                    .replace("-", "_")
+            )
+            .into(),
+            linker.display().to_string(),
+        ));
+        env_vars.push(("ANDROID_NDK_ROOT".into(), ndk.display().to_string()));
 
         if let Some(java_home) = java_home {
             tracing::debug!("Setting JAVA_HOME to {java_home:?}");
-            env_vars.push(("JAVA_HOME", java_home.display().to_string()));
+            env_vars.push(("JAVA_HOME".into(), java_home.display().to_string()));
         }
 
         // Set the wry env vars - this is where wry will dump its kotlin files.
         // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry.
-        env_vars.push(("WRY_ANDROID_PACKAGE", "dev.dioxus.main".to_string()));
-        env_vars.push(("WRY_ANDROID_LIBRARY", "dioxusmain".to_string()));
+        env_vars.push(("WRY_ANDROID_PACKAGE".into(), "dev.dioxus.main".to_string()));
+        env_vars.push(("WRY_ANDROID_LIBRARY".into(), "dioxusmain".to_string()));
         env_vars.push((
-            "WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
+            "WRY_ANDROID_KOTLIN_FILES_OUT_DIR".into(),
             self.wry_android_kotlin_files_out_dir()
                 .display()
                 .to_string(),
         ));
 
-        // Set the rust flags for android which get passed to *every* crate in the graph.
-        env_vars.push(("RUSTFLAGS", {
-            let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default();
-            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
-        }));
-
         // todo(jon): the guide for openssl recommends extending the path to include the tools dir
         //            in practice I couldn't get this to work, but this might eventually become useful.
         //
@@ -2138,7 +2470,11 @@ session_cache_dir: {}"#,
             .arg("-Z")
             .arg("unstable-options")
             .args(self.cargo_build_arguments(ctx))
-            .envs(self.cargo_build_env_vars(ctx)?)
+            .envs(
+                self.cargo_build_env_vars(ctx)?
+                    .iter()
+                    .map(|(k, v)| (k.as_ref(), v)),
+            )
             .output()
             .await?;
 
@@ -2274,7 +2610,7 @@ session_cache_dir: {}"#,
             android_bundle: Option<crate::AndroidSettings>,
         }
         let hbs_data = AndroidHandlebarsObjects {
-            application_id: self.full_mobile_app_name(),
+            application_id: self.bundle_identifier(),
             app_name: self.bundled_app_name(),
             android_bundle: self.config.bundle.android.clone(),
         };
@@ -2324,12 +2660,19 @@ session_cache_dir: {}"#,
             app.join("proguard-rules.pro"),
             include_bytes!("../../assets/android/gen/app/proguard-rules.pro"),
         )?;
-        write(
-            app.join("src").join("main").join("AndroidManifest.xml"),
-            hbs.render_template(
+
+        let manifest_xml = match self.config.application.android_manifest.as_deref() {
+            Some(manifest) => std::fs::read_to_string(self.package_manifest_dir().join(manifest))
+                .context("Failed to locate custom AndroidManifest.xml")?,
+            _ => hbs.render_template(
                 include_str!("../../assets/android/gen/app/src/main/AndroidManifest.xml.hbs"),
                 &hbs_data,
             )?,
+        };
+
+        write(
+            app.join("src").join("main").join("AndroidManifest.xml"),
+            manifest_xml,
         )?;
 
         // Write the main activity manually since tao dropped support for it
@@ -2459,7 +2802,7 @@ session_cache_dir: {}"#,
     /// is "distributed" after building an application (configurable in the
     /// `Dioxus.toml`).
     fn internal_out_dir(&self) -> PathBuf {
-        let dir = self.workspace_dir().join("target").join("dx");
+        let dir = self.target_dir.join("dx");
         std::fs::create_dir_all(&dir).unwrap();
         dir
     }
@@ -2473,7 +2816,7 @@ session_cache_dir: {}"#,
     /// target/dx/build/app/web/server.exe
     pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf {
         self.internal_out_dir()
-            .join(self.executable_name())
+            .join(&self.main_target)
             .join(if release { "release" } else { "debug" })
             .join(platform.build_folder_name())
     }
@@ -2484,7 +2827,7 @@ session_cache_dir: {}"#,
     /// target/dx/bundle/app/public/
     pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf {
         self.internal_out_dir()
-            .join(self.executable_name())
+            .join(&self.main_target)
             .join("bundle")
             .join(platform.build_folder_name())
     }
@@ -2560,54 +2903,6 @@ session_cache_dir: {}"#,
         })
     }
 
-    // The `opt-level=1` 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 top-level Cargo.toml file
-    // todo(jon): we should/could make these optional by placing some defaults somewhere
-    pub(crate) fn initialize_profiles(&self) -> crate::Result<()> {
-        let config_path = self.workspace_dir().join("Cargo.toml");
-        let mut config = match std::fs::read_to_string(&config_path) {
-            Ok(config) => config.parse::<toml_edit::DocumentMut>().map_err(|e| {
-                crate::Error::Other(anyhow::anyhow!("Failed to parse Cargo.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(1.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()));
-                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()));
-                entry.insert(Item::Table(android));
-            }
-        }
-
-        std::fs::write(config_path, config.to_string())
-            .context("Failed to write profiles to Cargo.toml")?;
-
-        Ok(())
-    }
-
     /// Return the version of the wasm-bindgen crate if it exists
     fn wasm_bindgen_version(&self) -> Option<String> {
         self.workspace
@@ -2657,8 +2952,6 @@ session_cache_dir: {}"#,
             return platforms;
         };
 
-        tracing::debug!("Default features: {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.
@@ -2688,8 +2981,6 @@ session_cache_dir: {}"#,
         platforms.sort();
         platforms.dedup();
 
-        tracing::debug!("Default platforms: {platforms:?}");
-
         platforms
     }
 
@@ -2731,30 +3022,26 @@ session_cache_dir: {}"#,
         kept_features
     }
 
-    pub(crate) fn mobile_org(&self) -> String {
-        let identifier = self.bundle_identifier();
-        let mut split = identifier.splitn(3, '.');
-        let sub = split
-            .next()
-            .expect("Identifier to have at least 3 periods like `com.example.app`");
-        let tld = split
-            .next()
-            .expect("Identifier to have at least 3 periods like `com.example.app`");
-        format!("{}.{}", sub, tld)
-    }
-
     pub(crate) fn bundled_app_name(&self) -> String {
         use convert_case::{Case, Casing};
         self.executable_name().to_case(Case::Pascal)
     }
 
-    pub(crate) fn full_mobile_app_name(&self) -> String {
-        format!("{}.{}", self.mobile_org(), self.bundled_app_name())
-    }
-
     pub(crate) fn bundle_identifier(&self) -> String {
-        if let Some(identifier) = self.config.bundle.identifier.clone() {
-            return identifier.clone();
+        if let Some(identifier) = &self.config.bundle.identifier {
+            if identifier.contains('.')
+                && !identifier.starts_with('.')
+                && !identifier.ends_with('.')
+                && !identifier.contains("..")
+            {
+                return identifier.clone();
+            } else {
+                // The original `mobile_org` function used `expect` directly.
+                // Maybe it's acceptable for the CLI to panic directly when this error occurs.
+                // And if we change it to a Result type, the `client_connected` function in serve/runner.rs does not return a Result and cannot call `?`,
+                // We also need to handle the error in place, otherwise it will expand the scope of modifications further.
+                panic!("Invalid bundle identifier: {identifier:?}. E.g. `com.example`, `com.example.app`");
+            }
         }
 
         format!("com.example.{}", self.bundled_app_name())
@@ -2876,13 +3163,15 @@ session_cache_dir: {}"#,
                 // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
                 let pre_compress = self.should_pre_compress_web_assets(self.release);
 
-                ctx.status_compressing_assets();
-                let asset_dir = self.asset_dir();
-                tokio::task::spawn_blocking(move || {
-                    crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
-                })
-                .await
-                .unwrap()?;
+                if pre_compress {
+                    ctx.status_compressing_assets();
+                    let asset_dir = self.asset_dir();
+                    tokio::task::spawn_blocking(move || {
+                        crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
+                    })
+                    .await
+                    .unwrap()?;
+                }
             }
             Platform::MacOS => {}
             Platform::Windows => {}
@@ -2899,7 +3188,12 @@ session_cache_dir: {}"#,
     /// 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.
     fn should_pre_compress_web_assets(&self, release: bool) -> bool {
-        self.config.web.pre_compress && release
+        self.config.web.pre_compress & release
+    }
+
+    /// Check if the wasm output should be bundled to an asset type app.
+    fn should_bundle_to_asset(&self) -> bool {
+        self.release && !self.wasm_split && self.platform == Platform::Web
     }
 
     /// Bundle the web app
@@ -2926,6 +3220,7 @@ session_cache_dir: {}"#,
             .expect("this should have been checked by tool verification");
 
         // Prepare any work dirs
+        _ = std::fs::remove_dir_all(&bindgen_outdir);
         std::fs::create_dir_all(&bindgen_outdir)?;
 
         // Lift the internal functions to exports
@@ -2942,8 +3237,7 @@ session_cache_dir: {}"#,
         //
         // We leave demangling to false since it's faster and these tools seem to prefer the raw symbols.
         // todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically.
-        let will_wasm_opt = (self.release || self.wasm_split)
-            && (self.workspace.wasm_opt.is_some() || cfg!(feature = "optimizations"));
+        let will_wasm_opt = self.release || self.wasm_split;
         let keep_debug = self.config.web.wasm_opt.debug
             || self.debug_symbols
             || self.wasm_split
@@ -2990,7 +3284,7 @@ session_cache_dir: {}"#,
 
             if !will_wasm_opt {
                 return Err(anyhow::anyhow!(
-                    "Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again."
+                    "Bundle splitting should automatically enable wasm-opt, but it was not enabled."
                 )
                 .into());
             }
@@ -3017,7 +3311,7 @@ session_cache_dir: {}"#,
                 writeln!(
                     glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);",
                     url = assets
-                        .register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
+                        .register_asset(&path, AssetOptions::builder().into_asset_options())?.bundled_path(),
                 )?;
             }
 
@@ -3045,7 +3339,8 @@ session_cache_dir: {}"#,
 
                     // Again, register this wasm with the asset system
                     url = assets
-                        .register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
+                        .register_asset(&path, AssetOptions::builder().into_asset_options())?
+                        .bundled_path(),
 
                     // This time, make sure to write the dependencies of this chunk
                     // The names here are again, hardcoded in wasm-split - fix this eventually.
@@ -3075,7 +3370,7 @@ session_cache_dir: {}"#,
             // Write the main wasm_bindgen file and register it with the asset system
             // This will overwrite the file in place
             // We will wasm-opt it in just a second...
-            std::fs::write(&post_bindgen_wasm, modules.main.bytes)?;
+            std::fs::write(&post_bindgen_wasm, modules.main.bytes).unwrap();
         }
 
         if matches!(ctx.mode, BuildMode::Fat) {
@@ -3091,38 +3386,113 @@ session_cache_dir: {}"#,
             wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
         }
 
-        // In release mode, we make the wasm and bindgen files into assets so they get bundled with max
-        // optimizations.
-        let wasm_path = if self.release {
+        if self.should_bundle_to_asset() {
+            // Make sure to register the main wasm file with the asset system
+            assets.register_asset(
+                &post_bindgen_wasm,
+                AssetOptions::builder().into_asset_options(),
+            )?;
+        }
+
+        // Now that the wasm is registered as an asset, we can write the js glue shim
+        self.write_js_glue_shim(assets)?;
+
+        if self.should_bundle_to_asset() {
             // Register the main.js with the asset system so it bundles in the snippets and optimizes
-            let name = assets.register_asset(
+            assets.register_asset(
                 &self.wasm_bindgen_js_output_file(),
-                AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
+                AssetOptions::js()
+                    .with_minify(true)
+                    .with_preload(true)
+                    .into_asset_options(),
             )?;
-            format!("assets/{}", name.bundled_path())
-        } else {
-            let asset = self.wasm_bindgen_wasm_output_file();
-            format!("wasm/{}", asset.file_name().unwrap().to_str().unwrap())
-        };
+        }
 
-        let js_path = if self.release {
-            // Make sure to register the main wasm file with the asset system
-            let name = assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
-            format!("assets/{}", name.bundled_path())
-        } else {
-            let asset = self.wasm_bindgen_js_output_file();
-            format!("wasm/{}", asset.file_name().unwrap().to_str().unwrap())
-        };
+        // Write the index.html file with the pre-configured contents we got from pre-rendering
+        self.write_index_html(assets)?;
+
+        Ok(())
+    }
+
+    fn write_js_glue_shim(&self, assets: &AssetManifest) -> Result<()> {
+        let wasm_path = self.bundled_wasm_path(assets);
+
+        // Load and initialize wasm without requiring a separate javascript file.
+        // This also allows using a strict Content-Security-Policy.
+        let mut js = std::fs::OpenOptions::new()
+            .append(true)
+            .open(self.wasm_bindgen_js_output_file())?;
+        let mut buf_writer = std::io::BufWriter::new(&mut js);
+        writeln!(
+            buf_writer,
+            r#"
+window.__wasm_split_main_initSync = initSync;
+
+// Actually perform the load
+__wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
+    // assign this module to be accessible globally
+    window.__dx_mainWasm = wasm;
+    window.__dx_mainInit = __wbg_init;
+    window.__dx_mainInitSync = initSync;
+    window.__dx___wbg_get_imports = __wbg_get_imports;
+
+    if (wasm.__wbindgen_start == undefined) {{
+        wasm.main();
+    }}
+}});
+"#,
+            self.base_path_or_default(),
+        )?;
+
+        Ok(())
+    }
+
+    /// Write the index.html file to the output directory. This must be called after the wasm and js
+    /// assets are registered with the asset system if this is a release build.
+    pub(crate) fn write_index_html(&self, assets: &AssetManifest) -> Result<()> {
+        let wasm_path = self.bundled_wasm_path(assets);
+        let js_path = self.bundled_js_path(assets);
 
         // Write the index.html file with the pre-configured contents we got from pre-rendering
         std::fs::write(
             self.root_dir().join("index.html"),
-            self.prepare_html(assets, &wasm_path, &js_path)?,
+            self.prepare_html(assets, &wasm_path, &js_path).unwrap(),
         )?;
 
         Ok(())
     }
 
+    fn bundled_js_path(&self, assets: &AssetManifest) -> String {
+        let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
+        if self.should_bundle_to_asset() {
+            let name = assets
+                .get_first_asset_for_source(&wasm_bindgen_js_out)
+                .expect("The js source must exist before creating index.html");
+            format!("assets/{}", name.bundled_path())
+        } else {
+            format!(
+                "wasm/{}",
+                wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
+            )
+        }
+    }
+
+    /// Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode
+    fn bundled_wasm_path(&self, assets: &AssetManifest) -> String {
+        let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
+        if self.should_bundle_to_asset() {
+            let name = assets
+                .get_first_asset_for_source(&wasm_bindgen_wasm_out)
+                .expect("The wasm source must exist before creating index.html");
+            format!("assets/{}", name.bundled_path())
+        } else {
+            format!(
+                "wasm/{}",
+                wasm_bindgen_wasm_out.file_name().unwrap().to_str().unwrap()
+            )
+        }
+    }
+
     fn info_plist_contents(&self, platform: Platform) -> Result<String> {
         #[derive(Serialize)]
         pub struct InfoPlistData {
@@ -3132,6 +3502,22 @@ session_cache_dir: {}"#,
             pub executable_name: String,
         }
 
+        // Attempt to use the user's manually specified
+        let _app = &self.config.application;
+        match platform {
+            Platform::MacOS => {
+                if let Some(macos_info_plist) = _app.macos_info_plist.as_deref() {
+                    return Ok(std::fs::read_to_string(macos_info_plist)?);
+                }
+            }
+            Platform::Ios => {
+                if let Some(macos_info_plist) = _app.ios_info_plist.as_deref() {
+                    return Ok(std::fs::read_to_string(macos_info_plist)?);
+                }
+            }
+            _ => {}
+        }
+
         match platform {
             Platform::MacOS => handlebars::Handlebars::new()
                 .render_template(
@@ -3257,10 +3643,10 @@ session_cache_dir: {}"#,
     ///
     /// It's not guaranteed that they're different from any other folder
     pub(crate) fn prepare_build_dir(&self) -> Result<()> {
-        use once_cell::sync::OnceCell;
         use std::fs::{create_dir_all, remove_dir_all};
+        use std::sync::OnceLock;
 
-        static INITIALIZED: OnceCell<Result<()>> = OnceCell::new();
+        static INITIALIZED: OnceLock<Result<()>> = OnceLock::new();
 
         let success = INITIALIZED.get_or_init(|| {
             if self.platform != Platform::Server {
@@ -3273,9 +3659,15 @@ session_cache_dir: {}"#,
             create_dir_all(self.exe_dir())?;
             create_dir_all(self.asset_dir())?;
 
-            tracing::debug!("Initialized Root dir: {:?}", self.root_dir());
-            tracing::debug!("Initialized Exe dir: {:?}", self.exe_dir());
-            tracing::debug!("Initialized Asset dir: {:?}", self.asset_dir());
+            tracing::debug!(
+                r#"Initialized build dirs:
+               • root dir: {:?}
+               • exe dir: {:?}
+               • asset dir: {:?}"#,
+                self.root_dir(),
+                self.exe_dir(),
+                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
@@ -3387,13 +3779,8 @@ session_cache_dir: {}"#,
     /// 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, ctx: &BuildContext) -> Result<()> {
-        tracing::debug!("Verifying tooling...");
         ctx.status_installing_tooling();
 
-        self
-            .initialize_profiles()
-            .context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?;
-
         match self.platform {
             Platform::Web => self.verify_web_tooling().await?,
             Platform::Ios => self.verify_ios_tooling().await?,
@@ -3503,7 +3890,7 @@ session_cache_dir: {}"#,
         Ok(())
     }
 
-    /// update the mtime of the "main" file to bust the fingerprint, forcing rustc to recompile it.
+    /// Blow away the fingerprint for this package, forcing rustc to recompile it.
     ///
     /// This prevents rustc from using the cached version of the binary, which can cause issues
     /// with our hotpatching setup since it uses linker interception.
@@ -3513,9 +3900,30 @@ session_cache_dir: {}"#,
     ///
     /// This might stop working if/when cargo stabilizes contents-based fingerprinting.
     fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
-        // if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base) {
-        if !matches!(ctx.mode, BuildMode::Thin { .. }) {
-            std::fs::File::open(&self.crate_target.src_path)?.set_modified(SystemTime::now())?;
+        if matches!(ctx.mode, BuildMode::Fat) {
+            // `dx` compiles everything with `--target` which ends up with a structure like:
+            // target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
+            //
+            // normally you can't rely on this structure (ie with `cargo build`) but the explicit
+            // target arg guarantees this will work.
+            let fingerprint_dir = self
+                .target_dir
+                .join(self.triple.to_string())
+                .join(&self.profile)
+                .join(".fingerprint");
+
+            // split at the last `-` used to separate the hash from the name
+            // This causes to more aggressively bust hashes for all combinations of features
+            // and fingerprints for this package since we're just ignoring the hash
+            for entry in std::fs::read_dir(&fingerprint_dir)?.flatten() {
+                if let Some(fname) = entry.file_name().to_str() {
+                    if let Some((name, _)) = fname.rsplit_once('-') {
+                        if name == self.package().name {
+                            _ = std::fs::remove_dir_all(entry.path());
+                        }
+                    }
+                }
+            }
         }
         Ok(())
     }
@@ -3567,10 +3975,10 @@ session_cache_dir: {}"#,
         };
 
         // Inject any resources from the config into the html
-        self.inject_resources(assets, &mut html)?;
+        self.inject_resources(assets, wasm_path, &mut html)?;
 
         // Inject loading scripts if they are not already present
-        self.inject_loading_scripts(&mut html);
+        self.inject_loading_scripts(assets, &mut html);
 
         // Replace any special placeholders in the HTML with resolved values
         self.replace_template_placeholders(&mut html, wasm_path, js_path);
@@ -3586,7 +3994,12 @@ session_cache_dir: {}"#,
     }
 
     // Inject any resources from the config into the html
-    fn inject_resources(&self, assets: &AssetManifest, html: &mut String) -> Result<()> {
+    fn inject_resources(
+        &self,
+        assets: &AssetManifest,
+        wasm_path: &str,
+        html: &mut String,
+    ) -> Result<()> {
         use std::fmt::Write;
 
         // Collect all resources into a list of styles and scripts
@@ -3627,24 +4040,24 @@ session_cache_dir: {}"#,
         }
 
         // Inject any resources from manganis into the head
-        for asset in assets.assets.values() {
+        for asset in assets.assets() {
             let asset_path = asset.bundled_path();
-            match asset.options() {
-                AssetOptions::Css(css_options) => {
+            match asset.options().variant() {
+                AssetVariant::Css(css_options) => {
                     if css_options.preloaded() {
                         head_resources.push_str(&format!(
                             "<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
                         ))
                     }
                 }
-                AssetOptions::Image(image_options) => {
+                AssetVariant::Image(image_options) => {
                     if image_options.preloaded() {
                         head_resources.push_str(&format!(
                             "<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
                         ))
                     }
                 }
-                AssetOptions::Js(js_options) => {
+                AssetVariant::Js(js_options) => {
                     if js_options.preloaded() {
                         head_resources.push_str(&format!(
                             "<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
@@ -3656,52 +4069,30 @@ session_cache_dir: {}"#,
         }
 
         // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
-        let wasm_source_path = self.wasm_bindgen_wasm_output_file();
-        if let Some(wasm_path) = assets.assets.get(&wasm_source_path) {
-            let wasm_path = wasm_path.bundled_path();
-            head_resources.push_str(&format!(
-                    "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
-                ));
-
-            Self::replace_or_insert_before("{style_include}", "</head", &head_resources, html);
-        }
+        head_resources.push_str(&format!(
+            "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/{wasm_path}\" crossorigin>"
+        ));
+        Self::replace_or_insert_before("{style_include}", "</head", &head_resources, html);
 
         Ok(())
     }
 
     /// 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.inject_loading_scripts || html.contains("__wbindgen_start") {
+    fn inject_loading_scripts(&self, assets: &AssetManifest, html: &mut String) {
+        // If the current build opted out of injecting loading scripts, don't inject anything
+        if !self.inject_loading_scripts {
             return;
         }
 
         // If not, insert the script
         *html = html.replace(
             "</body",
-r#" <script>
-  // We can't use a module script here because we need to start the script immediately when streaming
-  import("/{base_path}/{js_path}").then(
-    ({ default: init, initSync, __wbg_get_imports }) => {
-      // export initSync in case a split module needs to initialize
-      window.__wasm_split_main_initSync = initSync;
-
-      // Actually perform the load
-      init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => {
-        // assign this module to be accessible globally
-        window.__dx_mainWasm = wasm;
-        window.__dx_mainInit = init;
-        window.__dx_mainInitSync = initSync;
-        window.__dx___wbg_get_imports = __wbg_get_imports;
-
-        if (wasm.__wbindgen_start == undefined) {
-            wasm.main();
-        }
-      });
-    }
-  );
-  </script>
+            &format!(
+                r#"<script type="module" async src="/{}/{}"></script>
             </body"#,
+                self.base_path_or_default(),
+                self.bundled_js_path(assets)
+            ),
         );
     }
 
@@ -3741,11 +4132,9 @@ r#" <script>
 
     /// Get the base path from the config or None if this is not a web or server build
     pub(crate) fn base_path(&self) -> Option<&str> {
-        self.config
-            .web
-            .app
-            .base_path
+        self.base_path
             .as_deref()
+            .or(self.config.web.app.base_path.as_deref())
             .filter(|_| matches!(self.platform, Platform::Web | Platform::Server))
     }
 
@@ -3768,4 +4157,186 @@ r#" <script>
             .to_path_buf()
             .into()
     }
+
+    pub(crate) async fn start_simulators(&self) -> Result<()> {
+        if self.device {
+            return Ok(());
+        }
+
+        match self.platform {
+            // Boot an iOS simulator if one is not already running.
+            //
+            // We always choose the most recently opened simulator based on the xcrun list.
+            // Note that simulators can be running but the simulator app itself is not open.
+            // Calling `open::that` is always fine, even on running apps, since apps are singletons.
+            Platform::Ios => {
+                #[derive(Deserialize, Debug)]
+                struct XcrunListJson {
+                    // "com.apple.CoreSimulator.SimRuntime.iOS-18-4": [{}, {}, {}]
+                    devices: BTreeMap<String, Vec<XcrunDevice>>,
+                }
+
+                #[derive(Deserialize, Debug)]
+                struct XcrunDevice {
+                    #[serde(rename = "lastBootedAt")]
+                    last_booted_at: Option<String>,
+                    udid: String,
+                    name: String,
+                    state: String,
+                }
+                let xcrun_list = Command::new("xcrun")
+                    .arg("simctl")
+                    .arg("list")
+                    .arg("-j")
+                    .output()
+                    .await?;
+
+                let as_str = String::from_utf8_lossy(&xcrun_list.stdout);
+                let xcrun_list_json = serde_json::from_str::<XcrunListJson>(as_str.trim());
+                if let Ok(xcrun_list_json) = xcrun_list_json {
+                    if xcrun_list_json.devices.is_empty() {
+                        tracing::warn!(
+                            "No iOS sdks installed found. Please install the iOS SDK in Xcode."
+                        );
+                    }
+
+                    if let Some((_rt, devices)) = xcrun_list_json.devices.iter().next() {
+                        if devices.iter().all(|device| device.state != "Booted") {
+                            let last_booted =
+                                devices
+                                    .iter()
+                                    .max_by_key(|device| match device.last_booted_at {
+                                        Some(ref last_booted) => last_booted,
+                                        None => "2000-01-01T01:01:01Z",
+                                    });
+
+                            if let Some(device) = last_booted {
+                                tracing::info!("Booting iOS simulator: \"{}\"", device.name);
+                                Command::new("xcrun")
+                                    .arg("simctl")
+                                    .arg("boot")
+                                    .arg(&device.udid)
+                                    .output()
+                                    .await?;
+                            }
+                        }
+                    }
+                }
+                let path_to_xcode = Command::new("xcode-select")
+                    .arg("--print-path")
+                    .output()
+                    .await?;
+                let path_to_xcode: PathBuf = String::from_utf8_lossy(&path_to_xcode.stdout)
+                    .as_ref()
+                    .trim()
+                    .into();
+                let path_to_sim = path_to_xcode.join("Applications").join("Simulator.app");
+                open::that_detached(path_to_sim)?;
+            }
+
+            Platform::Android => {
+                let tools = self.workspace.android_tools()?;
+                tokio::spawn(async move {
+                    let emulator = tools.emulator();
+                    let avds = Command::new(&emulator)
+                        .arg("-list-avds")
+                        .output()
+                        .await
+                        .unwrap();
+                    let avds = String::from_utf8_lossy(&avds.stdout);
+                    let avd = avds.trim().lines().next().map(|s| s.trim().to_string());
+                    if let Some(avd) = avd {
+                        tracing::info!("Booting Android emulator: \"{avd}\"");
+                        Command::new(&emulator)
+                            .arg("-avd")
+                            .arg(avd)
+                            .args(["-netdelay", "none", "-netspeed", "full"])
+                            .stdout(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
+                            .stderr(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
+                            .output()
+                            .await
+                            .unwrap();
+                    } else {
+                        tracing::warn!("No Android emulators found. Please create one using `emulator -avd <name>`");
+                    }
+                });
+            }
+
+            _ => {
+                // nothing - maybe on the web we should open the browser?
+            }
+        };
+
+        Ok(())
+    }
+
+    fn select_ranlib(&self) -> Option<PathBuf> {
+        // prefer the modern llvm-ranlib if they have it
+        which::which("llvm-ranlib")
+            .or_else(|_| which::which("ranlib"))
+            .ok()
+    }
+
+    /// Assemble a series of `--config key=value` arguments for the build command.
+    ///
+    /// This adds adhoc profiles that dx uses to isolate builds from each other. Normally if you ran
+    /// `cargo build --feature desktop` and `cargo build --feature server`, then both binaries get
+    /// the same name and overwrite each other, causing thrashing and locking issues.
+    ///
+    /// By creating adhoc profiles, we can ensure that each build is isolated and doesn't interfere with each other.
+    ///
+    /// The user can also define custom profiles in their `Cargo.toml` file, which will be used instead
+    /// of the adhoc profiles.
+    ///
+    /// The names of the profiles are:
+    /// - web-dev
+    /// - web-release
+    /// - desktop-dev
+    /// - desktop-release
+    /// - server-dev
+    /// - server-release
+    /// - ios-dev
+    /// - ios-release
+    /// - android-dev
+    /// - android-release
+    /// - liveview-dev
+    /// - liveview-release
+    ///
+    /// Note how every platform gets its own profile, and each platform has a dev and release profile.
+    fn profile_args(&self) -> Vec<String> {
+        // If the user defined the profile in the Cargo.toml, we don't need to add it to our adhoc list
+        if self
+            .workspace
+            .cargo_toml
+            .profile
+            .custom
+            .contains_key(&self.profile)
+        {
+            return vec![];
+        }
+
+        // Otherwise, we need to add the profile arguments to make it adhoc
+        let mut args = Vec::new();
+
+        let profile = self.profile.as_str();
+        let inherits = if self.release { "release" } else { "dev" };
+
+        // Add the profile definition first.
+        args.push(format!(r#"profile.{profile}.inherits="{inherits}""#));
+
+        // The default dioxus experience is to lightly optimize the web build, both in debug and release
+        // Note that typically in release builds, you would strip debuginfo, but we actually choose to do
+        // that with wasm-opt tooling instead.
+        if matches!(self.platform, Platform::Web) {
+            match self.release {
+                true => args.push(r#"profile.web.opt-level="s""#.to_string()),
+                false => args.push(r#"profile.web.opt-level="1""#.to_string()),
+            }
+        }
+
+        // Prepend --config to each argument
+        args.into_iter()
+            .flat_map(|arg| ["--config".to_string(), arg])
+            .collect()
+    }
 }

+ 20 - 4
packages/cli/src/build/tools.rs

@@ -8,9 +8,10 @@ use tokio::process::Command;
 
 /// The tools for Android (ndk, sdk, etc)
 ///
-/// https://gist.github.com/Pulimet/5013acf2cd5b28e55036c82c91bd56d8?permalink_comment_id=3678614
+/// <https://gist.github.com/Pulimet/5013acf2cd5b28e55036c82c91bd56d8?permalink_comment_id=3678614>
 #[derive(Debug, Clone)]
 pub(crate) struct AndroidTools {
+    pub(crate) sdk: Option<PathBuf>,
     pub(crate) ndk: PathBuf,
     pub(crate) adb: PathBuf,
     pub(crate) java_home: Option<PathBuf>,
@@ -114,6 +115,7 @@ pub fn get_android_tools() -> Option<Arc<AndroidTools>> {
         ndk,
         adb,
         java_home,
+        sdk,
     }))
 }
 
@@ -168,6 +170,18 @@ impl AndroidTools {
         ))
     }
 
+    pub(crate) fn sdk(&self) -> PathBuf {
+        // /Users/jonathankelley/Library/Android/sdk/ndk/25.2/... (25.2 is the ndk here)
+        // /Users/jonathankelley/Library/Android/sdk/
+        self.sdk
+            .clone()
+            .unwrap_or_else(|| self.ndk.parent().unwrap().parent().unwrap().to_path_buf())
+    }
+
+    pub(crate) fn emulator(&self) -> PathBuf {
+        self.sdk().join("emulator").join("emulator")
+    }
+
     // todo(jon): this should be configurable
     pub(crate) fn min_sdk_version(&self) -> u32 {
         24
@@ -230,8 +244,11 @@ impl AndroidTools {
                 "x86_64" => {
                     triple.architecture = Architecture::X86_64;
                 }
+                "" => {
+                    tracing::debug!("No device running - probably waiting for emulator");
+                }
                 other => {
-                    tracing::warn!("Unknown architecture from adb: {other}");
+                    tracing::debug!("Unknown architecture from adb: {other}");
                 }
             },
             Ok(Err(err)) => {
@@ -248,10 +265,9 @@ impl AndroidTools {
 
 fn var_or_debug(name: &str) -> Option<PathBuf> {
     use std::env::var;
-    use tracing::debug;
 
     var(name)
-        .inspect_err(|_| debug!("{name} not set"))
+        .inspect_err(|_| tracing::trace!("{name} not set"))
         .ok()
         .map(PathBuf::from)
 }

+ 69 - 116
packages/cli/src/cli/build.rs

@@ -1,8 +1,7 @@
-use crate::{cli::*, AppBuilder, BuildRequest, Workspace, PROFILE_SERVER};
+use crate::{cli::*, AppBuilder, BuildRequest, Workspace};
 use crate::{BuildMode, Platform};
-use target_lexicon::Triple;
 
-use super::target::{TargetArgs, TargetCmd};
+use super::target::TargetArgs;
 
 /// Build the Rust Dioxus app and all of its assets.
 ///
@@ -16,42 +15,13 @@ pub struct BuildArgs {
     #[clap(long)]
     pub(crate) fullstack: Option<bool>,
 
-    /// The feature to use for the client in a fullstack app [default: "web"]
+    /// Pre-render all routes returned from the app's `/static_routes` endpoint [default: false]
     #[clap(long)]
-    pub(crate) client_features: Vec<String>,
-
-    /// The feature to use for the server in a fullstack app [default: "server"]
-    #[clap(long)]
-    pub(crate) server_features: Vec<String>,
-
-    /// Build with custom profile for the fullstack server
-    #[clap(long, default_value_t = PROFILE_SERVER.to_string())]
-    pub(crate) server_profile: String,
-
-    /// The target to build for the server.
-    ///
-    /// This can be different than the host allowing cross-compilation of the server. This is useful for
-    /// platforms like Cloudflare Workers where the server is compiled to wasm and then uploaded to the edge.
-    #[clap(long)]
-    pub(crate) server_target: Option<Triple>,
+    pub(crate) ssg: bool,
 
     /// Arguments for the build itself
     #[clap(flatten)]
     pub(crate) build_arguments: TargetArgs,
-
-    /// A list of additional targets to build.
-    ///
-    /// Server and Client are special targets that integrate with `dx serve`, while `crate` is a generic.
-    ///
-    /// ```
-    /// dx serve \
-    ///     client --target aarch64-apple-darwin \
-    ///     server --target wasm32-unknown-unknown \
-    ///     crate --target aarch64-unknown-linux-gnu --package foo \
-    ///     crate --target x86_64-unknown-linux-gnu --package bar
-    /// ```
-    #[command(subcommand)]
-    pub(crate) targets: Option<TargetCmd>,
 }
 
 pub struct BuildTargets {
@@ -60,9 +30,44 @@ pub struct BuildTargets {
 }
 
 impl BuildArgs {
+    fn default_client(&self) -> &TargetArgs {
+        &self.build_arguments
+    }
+
+    fn default_server(&self, client: &BuildRequest) -> Option<&TargetArgs> {
+        // Now resolve the builds that we need to.
+        // These come from the args, but we'd like them to come from the `TargetCmd` chained object
+        //
+        // The process here is as follows:
+        //
+        // - Create the BuildRequest for the primary target
+        // - If that BuildRequest is "fullstack", then add the client features
+        // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server
+        //   with the server features
+        //
+        // This involves modifying the BuildRequest to add the client features and server features
+        // only if we can properly detect that it's a fullstack build. Careful with this, since
+        // we didn't build BuildRequest to be generally mutable.
+        let default_server = client.enabled_platforms.contains(&Platform::Server);
+
+        // 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
+        //
+        // Now we need to resolve the client features
+        let fullstack = ((default_server || client.fullstack_feature_enabled())
+            || self.fullstack.unwrap_or(false))
+            && self.fullstack != Some(false);
+
+        fullstack.then_some(&self.build_arguments)
+    }
+}
+
+impl CommandWithPlatformOverrides<BuildArgs> {
     pub async fn build(self) -> Result<StructuredOutput> {
         tracing::info!("Building project...");
 
+        let ssg = self.shared.ssg;
         let targets = self.into_targets().await?;
 
         AppBuilder::start(&targets.client, BuildMode::Base)?
@@ -73,9 +78,13 @@ impl BuildArgs {
 
         if let Some(server) = targets.server.as_ref() {
             // If the server is present, we need to build it as well
-            AppBuilder::start(server, BuildMode::Base)?
-                .finish_build()
-                .await?;
+            let mut server_build = AppBuilder::start(server, BuildMode::Base)?;
+            server_build.finish_build().await?;
+
+            // Run SSG and cache static routes
+            if ssg {
+                crate::pre_render_static_routes(None, &mut server_build, None).await?;
+            }
 
             tracing::info!(path = ?targets.client.root_dir(), "Server build completed successfully! 🚀");
         }
@@ -89,89 +98,33 @@ impl BuildArgs {
     pub async fn into_targets(self) -> Result<BuildTargets> {
         let workspace = Workspace::current().await?;
 
-        let mut server = None;
+        // do some logging to ensure dx matches the dioxus version since we're not always API compatible
+        workspace.check_dioxus_version_against_cli();
 
-        let client = match self.targets {
-            // A simple `dx serve` command with no explicit targets
-            None => {
-                // Now resolve the builds that we need to.
-                // These come from the args, but we'd like them to come from the `TargetCmd` chained object
-                //
-                // The process here is as follows:
-                //
-                // - Create the BuildRequest for the primary target
-                // - If that BuildRequest is "fullstack", then add the client features
-                // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server
-                //   with the server features
-                //
-                // This involves modifying the BuildRequest to add the client features and server features
-                // only if we can properly detect that it's a fullstack build. Careful with this, since
-                // we didn't build BuildRequest to be generally mutable.
-                let client = BuildRequest::new(&self.build_arguments, workspace.clone()).await?;
-                let default_server = client
-                    .enabled_platforms
-                    .iter()
-                    .any(|p| *p == Platform::Server);
-
-                // 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
-                //
-                // Now we need to resolve the client features
-                let fullstack = ((default_server || client.fullstack_feature_enabled())
-                    || self.fullstack.unwrap_or(false))
-                    && self.fullstack != Some(false);
-
-                if fullstack {
-                    let mut build_args = self.build_arguments.clone();
-                    build_args.platform = Some(Platform::Server);
-
-                    let _server = BuildRequest::new(&build_args, workspace.clone()).await?;
-
-                    // ... todo: add the server features to the server build
-                    // ... todo: add the client features to the client build
-                    // // Make sure we have a server feature if we're building a fullstack app
-                    if self.fullstack.unwrap_or_default() && self.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());
-                    }
-
-                    server = Some(_server);
-                }
-
-                client
-            }
+        let client_args = match &self.client {
+            Some(client) => &client.build_arguments,
+            None => self.shared.default_client(),
+        };
+        let client = BuildRequest::new(client_args, None, workspace.clone()).await?;
 
-            // A command in the form of:
-            // ```
-            // dx serve \
-            //     client --package frontend \
-            //     server --package backend
-            // ```
-            Some(cmd) => {
-                let mut client_args_ = None;
-                let mut server_args_ = None;
-                let mut cmd_outer = Some(Box::new(cmd));
-                while let Some(cmd) = cmd_outer.take() {
-                    match *cmd {
-                        TargetCmd::Client(cmd_) => {
-                            client_args_ = Some(cmd_.inner);
-                            cmd_outer = cmd_.next;
-                        }
-                        TargetCmd::Server(cmd) => {
-                            server_args_ = Some(cmd.inner);
-                            cmd_outer = cmd.next;
-                        }
-                    }
-                }
-
-                if let Some(server_args) = server_args_ {
-                    server = Some(BuildRequest::new(&server_args, workspace.clone()).await?);
-                }
-
-                BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await?
-            }
+        let server_args = match &self.server {
+            Some(server) => Some(&server.build_arguments),
+            None => self.shared.default_server(&client),
         };
 
+        let mut server = None;
+        // If there is a server, make sure we output in the same directory as the client build so we use the server
+        // to serve the web client
+        if let Some(server_args) = server_args {
+            // Copy the main target from the client to the server
+            let main_target = client.main_target.clone();
+            let mut server_args = server_args.clone();
+            // The platform in the server build is always set to Server
+            server_args.platform = Some(Platform::Server);
+            server =
+                Some(BuildRequest::new(&server_args, Some(main_target), workspace.clone()).await?);
+        }
+
         Ok(BuildTargets { client, server })
     }
 }

+ 5 - 9
packages/cli/src/cli/build_assets.rs

@@ -1,8 +1,8 @@
 use std::{fs::create_dir_all, path::PathBuf};
 
-use crate::{Result, StructuredOutput};
+use crate::{extract_assets_from_file, Result, StructuredOutput};
 use clap::Parser;
-use dioxus_cli_opt::{process_file_to, AssetManifest};
+use dioxus_cli_opt::process_file_to;
 use tracing::debug;
 
 #[derive(Clone, Debug, Parser)]
@@ -10,21 +10,17 @@ pub struct BuildAssets {
     /// The source executable to build assets for.
     pub(crate) executable: PathBuf,
 
-    /// The source directory for the assets.
-    pub(crate) source: PathBuf,
-
     /// The destination directory for the assets.
     pub(crate) destination: PathBuf,
 }
 
 impl BuildAssets {
     pub async fn run(self) -> Result<StructuredOutput> {
-        let mut manifest = AssetManifest::default();
-        manifest.add_from_object_path(&self.executable)?;
+        let manifest = extract_assets_from_file(&self.executable)?;
 
         create_dir_all(&self.destination)?;
-        for (path, asset) in manifest.assets.iter() {
-            let source_path = self.source.join(path);
+        for asset in manifest.assets() {
+            let source_path = PathBuf::from(asset.absolute_source_path());
             let destination_path = self.destination.join(asset.bundled_path());
             debug!(
                 "Processing asset {} --> {} {:#?}",

+ 2 - 148
packages/cli/src/cli/bundle.rs

@@ -1,17 +1,9 @@
 use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, Platform};
 use anyhow::{anyhow, Context};
-use dioxus_cli_config::{server_ip, server_port};
-use futures_util::stream::FuturesUnordered;
-use futures_util::StreamExt;
 use path_absolutize::Absolutize;
 use std::collections::HashMap;
-use std::{
-    net::{IpAddr, Ipv4Addr, SocketAddr},
-    path::Path,
-    time::Duration,
-};
 use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
-use tokio::process::Command;
+
 use walkdir::WalkDir;
 
 use super::*;
@@ -35,19 +27,9 @@ pub struct Bundle {
     #[clap(long)]
     pub out_dir: Option<PathBuf>,
 
-    /// 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,
-
     /// The arguments for the dioxus build
     #[clap(flatten)]
-    pub(crate) args: BuildArgs,
+    pub(crate) args: CommandWithPlatformOverrides<BuildArgs>,
 }
 
 impl Bundle {
@@ -153,17 +135,6 @@ impl Bundle {
             );
         }
 
-        // Run SSG and cache static routes
-        if self.ssg {
-            if let Some(server) = server.as_ref() {
-                tracing::info!("Running SSG for static routes...");
-                Self::pre_render_static_routes(&server.main_exe()).await?;
-                tracing::info!("SSG complete");
-            } else {
-                tracing::error!("SSG is only supported for fullstack apps. Ensure you have the server feature enabled and try again.");
-            }
-        }
-
         Ok(StructuredOutput::BundleOutput { bundles })
     }
 
@@ -288,121 +259,4 @@ impl Bundle {
 
         Ok(bundles)
     }
-
-    /// Pre-render the static routes, performing static-site generation
-    async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Result<()> {
-        // Use the address passed in through environment variables or default to localhost:9999. We need
-        // to default to a value that is different than the CLI default address to avoid conflicts
-        let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
-        let port = server_port().unwrap_or(9999);
-        let fullstack_address = SocketAddr::new(ip, port);
-        let address = fullstack_address.ip().to_string();
-        let port = fullstack_address.port().to_string();
-
-        // Borrow port and address so we can easily moe them into multiple tasks below
-        let address = &address;
-        let port = &port;
-
-        tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
-
-        // Run the server executable
-        let _child = Command::new(server_exe)
-            .env(dioxus_cli_config::SERVER_PORT_ENV, port)
-            .env(dioxus_cli_config::SERVER_IP_ENV, address)
-            .current_dir(server_exe.parent().unwrap())
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .kill_on_drop(true)
-            .spawn()?;
-
-        // Borrow reqwest_client so we only move the reference into the futures
-        let reqwest_client = reqwest::Client::new();
-        let reqwest_client = &reqwest_client;
-
-        // Get the routes from the `/static_routes` endpoint
-        let mut routes = None;
-
-        // The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
-        const RETRY_ATTEMPTS: usize = 5;
-        for i in 0..=RETRY_ATTEMPTS {
-            tracing::debug!(
-                "Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}"
-            );
-
-            let request = reqwest_client
-                .post(format!("http://{address}:{port}/api/static_routes"))
-                .body("{}".to_string())
-                .send()
-                .await;
-            match request {
-                Ok(request) => {
-                    routes = Some(request
-                    .json::<Vec<String>>()
-                    .await
-                    .inspect(|text| tracing::debug!("Got static routes: {text:?}"))
-                    .context("Failed to parse static routes from the server. Make sure your server function returns Vec<String> with the (default) json encoding")?);
-                    break;
-                }
-                Err(err) => {
-                    // If the request fails, try  up to 5 times with a one second delay
-                    // If it fails 5 times, return the error
-                    if i == RETRY_ATTEMPTS {
-                        return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec<String> of static routes.");
-                    }
-                    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
-                }
-            }
-        }
-
-        let routes = routes.expect(
-            "static routes should exist or an error should have been returned on the last attempt",
-        );
-
-        // Create a pool of futures that cache each route
-        let mut resolved_routes = routes
-            .into_iter()
-            .map(|route| async move {
-                tracing::info!("Rendering {route} for SSG");
-
-                // For each route, ping the server to force it to cache the response for ssg
-                let request = reqwest_client
-                    .get(format!("http://{address}:{port}{route}"))
-                    .header("Accept", "text/html")
-                    .send()
-                    .await?;
-
-                // If it takes longer than 30 seconds to resolve the route, log a warning
-                let warning_task = tokio::spawn({
-                    let route = route.clone();
-                    async move {
-                        tokio::time::sleep(Duration::from_secs(30)).await;
-                        tracing::warn!("Route {route} has been rendering for 30 seconds");
-                    }
-                });
-
-                // Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
-                // because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
-                // the final clean HTML to the disk automatically after the request completes.
-                let _html = request.text().await?;
-
-                // Cancel the warning task if it hasn't already run
-                warning_task.abort();
-
-                Ok::<_, reqwest::Error>(route)
-            })
-            .collect::<FuturesUnordered<_>>();
-
-        while let Some(route) = resolved_routes.next().await {
-            match route {
-                Ok(route) => tracing::debug!("ssg success: {route:?}"),
-                Err(err) => tracing::error!("ssg error: {err:?}"),
-            }
-        }
-
-        tracing::info!("SSG complete");
-
-        drop(_child);
-
-        Ok(())
-    }
 }

+ 3 - 5
packages/cli/src/cli/check.rs

@@ -1,14 +1,13 @@
 //! Run linting against the user's codebase.
 //!
 //! For reference, the rustfmt main.rs file
-//! https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
+//! <https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs>
 
 use super::*;
 use crate::BuildRequest;
 use anyhow::Context;
 use futures_util::{stream::FuturesUnordered, StreamExt};
 use std::path::Path;
-use walkdir::WalkDir;
 
 /// Check the Rust files in the project for issues.
 #[derive(Clone, Debug, Parser)]
@@ -19,7 +18,7 @@ pub(crate) struct Check {
 
     /// Information about the target to check
     #[clap(flatten)]
-    pub(crate) build_args: BuildArgs,
+    pub(crate) build_args: CommandWithPlatformOverrides<BuildArgs>,
 }
 
 impl Check {
@@ -125,8 +124,7 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
 }
 
 pub(crate) fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
-    let dir = WalkDir::new(folder).follow_links(true).into_iter();
-    for entry in dir.flatten() {
+    for entry in ignore::Walk::new(folder).flatten() {
         if entry.path().extension() == Some("rs".as_ref()) {
             files.push(entry.path().to_path_buf());
         }

+ 63 - 9
packages/cli/src/cli/create.rs

@@ -1,6 +1,6 @@
 use super::*;
 use crate::TraceSrc;
-use cargo_generate::{GenerateArgs, TemplatePath};
+use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
 use std::path::Path;
 
 pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template";
@@ -38,7 +38,7 @@ pub struct Create {
     #[clap(long)]
     subtemplate: Option<String>,
 
-    /// Pass <option>=<value> for the used template (e.g., `foo=bar`)
+    /// Pass `<option>=<value>` for the used template (e.g., `foo=bar`)
     #[clap(short, long)]
     option: Vec<String>,
 
@@ -46,15 +46,25 @@ pub struct Create {
     /// Default values can be overridden with `--option`
     #[clap(short, long)]
     yes: bool,
+
+    /// Specify the VCS used to initialize the generated template.
+    /// Options: `git`, `none`.
+    #[arg(long, value_parser)]
+    vcs: Option<Vcs>,
 }
 
 impl Create {
-    pub fn create(mut self) -> Result<StructuredOutput> {
+    pub async fn create(mut self) -> Result<StructuredOutput> {
         // Project name defaults to directory name.
         if self.name.is_none() {
             self.name = Some(create::name_from_path(&self.path)?);
         }
 
+        // Perform a connectivity check so we just don't it around doing nothing if there's a network error
+        if self.template.is_none() {
+            connectivity_check().await?;
+        }
+
         // If no template is specified, use the default one and set the branch to the latest release.
         resolve_template_and_branch(&mut self.template, &mut self.branch);
 
@@ -72,6 +82,7 @@ impl Create {
             init: true,
             name: self.name,
             silent: self.yes,
+            vcs: self.vcs,
             template_path: TemplatePath {
                 auto_path: self.template,
                 branch: self.branch,
@@ -91,7 +102,7 @@ impl Create {
         tracing::debug!(dx_src = ?TraceSrc::Dev, "Creating new project with args: {args:#?}");
         let path = cargo_generate::generate(args)?;
 
-        _ = post_create(&path);
+        _ = post_create(&path, &self.vcs.unwrap_or(Vcs::Git));
 
         Ok(StructuredOutput::Success)
     }
@@ -117,7 +128,7 @@ pub(crate) fn resolve_template_and_branch(
 /// Prevent hidden cursor if Ctrl+C is pressed when interacting
 /// with cargo-generate's prompts.
 ///
-/// See https://github.com/DioxusLabs/dioxus/pull/2603.
+/// See <https://github.com/DioxusLabs/dioxus/pull/2603>.
 pub(crate) fn restore_cursor_on_sigint() {
     ctrlc::set_handler(move || {
         if let Err(err) = console::Term::stdout().show_cursor() {
@@ -143,7 +154,7 @@ pub(crate) fn name_from_path(path: &Path) -> Result<String> {
 }
 
 /// Post-creation actions for newly setup crates.
-pub(crate) fn post_create(path: &Path) -> Result<()> {
+pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
     let parent_dir = path.parent();
     let metadata = if parent_dir.is_none() {
         None
@@ -166,6 +177,7 @@ pub(crate) fn post_create(path: &Path) -> 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.
+    let is_workspace = metadata.is_some();
     metadata.and_then(|metadata| {
         let cargo_toml_path = &metadata.workspace_root.join("Cargo.toml");
         let cargo_toml_str = std::fs::read_to_string(cargo_toml_path).ok()?;
@@ -221,7 +233,12 @@ pub(crate) fn post_create(path: &Path) -> Result<()> {
     let mut file = std::fs::File::create(readme_path)?;
     file.write_all(new_readme.as_bytes())?;
 
-    tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}\n\n`cd` to your project and run `dx serve` to start developing.\nIf using Tailwind, make sure to run the Tailwind CLI.\nMore information is available in the generated `README.md`.\n\nBuild cool things! ✌️", path.display());
+    // 5. Run git init
+    if !is_workspace {
+        vcs.initialize(path, Some("main"), true)?;
+    }
+
+    tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}\n\n`cd` to your project and run `dx serve` to start developing.\nMore information is available in the generated `README.md`.\n\nBuild cool things! ✌️", path.display());
 
     Ok(())
 }
@@ -237,19 +254,56 @@ fn remove_triple_newlines(string: &str) -> String {
     new_string
 }
 
+/// Perform a health check against github itself before we attempt to download any templates hosted
+/// on github.
+pub(crate) async fn connectivity_check() -> Result<()> {
+    if crate::VERBOSITY
+        .get()
+        .map(|f| f.offline)
+        .unwrap_or_default()
+    {
+        return Ok(());
+    }
+
+    use crate::styles::{GLOW_STYLE, LINK_STYLE};
+    let client = reqwest::Client::new();
+    for x in 0..=5 {
+        tokio::select! {
+            res = client.head("https://github.com/DioxusLabs/").header("User-Agent", "dioxus-cli").send() => {
+                if res.is_ok() {
+                    return Ok(());
+                }
+                tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
+            },
+            _ = tokio::time::sleep(std::time::Duration::from_millis(if x == 1 { 500 } else { 2000 })) => {}
+        }
+        if x == 0 {
+            println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for {LINK_STYLE}https://github.com/dioxuslabs{LINK_STYLE:#}...")
+        } else {
+            println!(
+                "{GLOW_STYLE}warning{GLOW_STYLE:#}: ({x}/5) Taking a while, maybe your internet is down?"
+            );
+        }
+    }
+
+    Err(Error::Network(
+        "Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project.".to_string(),
+    ))
+}
+
 // todo: re-enable these tests with better parallelization
 //
 // #[cfg(test)]
 // pub(crate) mod tests {
 //     use escargot::{CargoBuild, CargoRun};
-//     use once_cell::sync::Lazy;
+//     use std::sync::LazyLock;
 //     use std::fs::{create_dir_all, read_to_string};
 //     use std::path::{Path, PathBuf};
 //     use std::process::Command;
 //     use tempfile::tempdir;
 //     use toml::Value;
 
-//     static BINARY: Lazy<CargoRun> = Lazy::new(|| {
+//     static BINARY: LazyLock<CargoRun> = LazyLock::new(|| {
 //         CargoBuild::new()
 //             .bin(env!("CARGO_BIN_NAME"))
 //             .current_release()

+ 15 - 4
packages/cli/src/cli/init.rs

@@ -1,5 +1,5 @@
 use super::*;
-use cargo_generate::{GenerateArgs, TemplatePath};
+use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
 
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 #[clap(name = "init")]
@@ -35,7 +35,7 @@ pub struct Init {
     #[clap(long)]
     subtemplate: Option<String>,
 
-    /// Pass <option>=<value> for the used template (e.g., `foo=bar`)
+    /// Pass `<option>=<value>` for the used template (e.g., `foo=bar`)
     #[clap(short, long)]
     option: Vec<String>,
 
@@ -43,15 +43,25 @@ pub struct Init {
     /// Default values can be overridden with `--option`
     #[clap(short, long)]
     yes: bool,
+
+    /// Specify the VCS used to initialize the generated template.
+    /// Options: `git`, `none`.
+    #[arg(long, value_parser)]
+    vcs: Option<Vcs>,
 }
 
 impl Init {
-    pub fn init(mut self) -> Result<StructuredOutput> {
+    pub async fn init(mut self) -> Result<StructuredOutput> {
         // Project name defaults to directory name.
         if self.name.is_none() {
             self.name = Some(create::name_from_path(&self.path)?);
         }
 
+        // Perform a connectivity check so we just don't it around doing nothing if there's a network error
+        if self.template.is_none() {
+            create::connectivity_check().await?;
+        }
+
         // If no template is specified, use the default one and set the branch to the latest release.
         create::resolve_template_and_branch(&mut self.template, &mut self.branch);
 
@@ -64,6 +74,7 @@ impl Init {
             init: true,
             name: self.name,
             silent: self.yes,
+            vcs: self.vcs,
             template_path: TemplatePath {
                 auto_path: self.template,
                 branch: self.branch,
@@ -76,7 +87,7 @@ impl Init {
         };
         create::restore_cursor_on_sigint();
         let path = cargo_generate::generate(args)?;
-        _ = create::post_create(&path);
+        _ = create::post_create(&path, &self.vcs.unwrap_or(Vcs::Git));
         Ok(StructuredOutput::Success)
     }
 }

+ 27 - 15
packages/cli/src/cli/link.rs

@@ -1,7 +1,7 @@
 use crate::Result;
 use anyhow::Context;
 use serde::{Deserialize, Serialize};
-use std::path::PathBuf;
+use std::{borrow::Cow, path::PathBuf};
 use target_lexicon::Triple;
 
 /// `dx` can act as a linker in a few scenarios. Note that we don't *actually* implement the linker logic,
@@ -38,14 +38,14 @@ pub struct LinkAction {
 /// The linker flavor to use. This influences the argument style that gets passed to the linker.
 /// We're imitating the rustc linker flavors here.
 ///
-/// https://doc.rust-lang.org/beta/nightly-rustc/rustc_target/spec/enum.LinkerFlavor.html
+/// <https://doc.rust-lang.org/beta/nightly-rustc/rustc_target/spec/enum.LinkerFlavor.html>
 #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
 pub enum LinkerFlavor {
     Gnu,
     Darwin,
     WasmLld,
-    Unix,
     Msvc,
+    Unsupported, // a catch-all for unsupported linkers, usually the stripped-down unix ones
 }
 
 impl LinkAction {
@@ -80,25 +80,31 @@ impl LinkAction {
         })
     }
 
-    pub(crate) fn write_env_vars(&self, env_vars: &mut Vec<(&str, String)>) -> Result<()> {
-        env_vars.push((Self::DX_LINK_ARG, "1".to_string()));
+    pub(crate) fn write_env_vars(
+        &self,
+        env_vars: &mut Vec<(Cow<'static, str>, String)>,
+    ) -> Result<()> {
+        env_vars.push((Self::DX_LINK_ARG.into(), "1".to_string()));
         env_vars.push((
-            Self::DX_ARGS_FILE,
+            Self::DX_ARGS_FILE.into(),
             dunce::canonicalize(&self.link_args_file)?
                 .to_string_lossy()
                 .to_string(),
         ));
         env_vars.push((
-            Self::DX_ERR_FILE,
+            Self::DX_ERR_FILE.into(),
             dunce::canonicalize(&self.link_err_file)?
                 .to_string_lossy()
                 .to_string(),
         ));
-        env_vars.push((Self::DX_LINK_TRIPLE, self.triple.to_string()));
+        env_vars.push((Self::DX_LINK_TRIPLE.into(), self.triple.to_string()));
         if let Some(linker) = &self.linker {
             env_vars.push((
-                Self::DX_LINK_CUSTOM_LINKER,
-                dunce::canonicalize(linker)?.to_string_lossy().to_string(),
+                Self::DX_LINK_CUSTOM_LINKER.into(),
+                dunce::canonicalize(linker)
+                    .unwrap_or(linker.clone())
+                    .to_string_lossy()
+                    .to_string(),
             ));
         }
 
@@ -129,18 +135,24 @@ impl LinkAction {
 
         handle_linker_command_file(&mut args);
 
+        if self.triple.environment == target_lexicon::Environment::Android {
+            args.retain(|arg| !arg.ends_with(".lib"));
+        }
+
         // Write the linker args to a file for the main process to read
         // todo: we might need to encode these as escaped shell words in case newlines are passed
-        std::fs::write(self.link_args_file, args.join("\n"))?;
+        std::fs::write(&self.link_args_file, args.join("\n"))?;
 
         // If there's a linker specified, we use that. Otherwise, we write a dummy object file to satisfy
         // any post-processing steps that rustc does.
         match self.linker {
             Some(linker) => {
-                let res = std::process::Command::new(linker)
-                    .args(args.iter().skip(1))
-                    .output()
-                    .expect("Failed to run linker");
+                let mut cmd = std::process::Command::new(linker);
+                match cfg!(target_os = "windows") {
+                    true => cmd.arg(format!("@{}", &self.link_args_file.display())),
+                    false => cmd.args(args.iter().skip(1)),
+                };
+                let res = cmd.output().expect("Failed to run linker");
 
                 if !res.stderr.is_empty() || !res.stdout.is_empty() {
                     _ = std::fs::create_dir_all(self.link_err_file.parent().unwrap());

+ 70 - 22
packages/cli/src/cli/mod.rs

@@ -8,10 +8,12 @@ pub(crate) mod config;
 pub(crate) mod create;
 pub(crate) mod init;
 pub(crate) mod link;
+pub(crate) mod platform_override;
 pub(crate) mod run;
 pub(crate) mod serve;
 pub(crate) mod target;
 pub(crate) mod translate;
+pub(crate) mod update;
 pub(crate) mod verbosity;
 
 pub(crate) use build::*;
@@ -19,11 +21,13 @@ pub(crate) use serve::*;
 pub(crate) use target::*;
 pub(crate) use verbosity::*;
 
+use crate::platform_override::CommandWithPlatformOverrides;
 use crate::{error::Result, Error, StructuredOutput};
+use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
 use clap::{Parser, Subcommand};
 use html_parser::Dom;
-use once_cell::sync::Lazy;
 use serde::Deserialize;
+use std::sync::LazyLock;
 use std::{
     fmt::Display,
     fs::File,
@@ -32,9 +36,10 @@ use std::{
     process::Command,
 };
 
-/// Build, Bundle & Ship Dioxus Apps.
+/// Dioxus: build web, desktop, and mobile apps with a single codebase.
 #[derive(Parser)]
 #[clap(name = "dioxus", version = VERSION.as_str())]
+#[clap(styles = CARGO_STYLING)]
 pub(crate) struct Cli {
     #[command(subcommand)]
     pub(crate) action: Commands,
@@ -45,21 +50,25 @@ pub(crate) struct Cli {
 
 #[derive(Subcommand)]
 pub(crate) enum Commands {
-    /// Build the Dioxus project and all of its assets.
-    #[clap(name = "build")]
-    Build(build::BuildArgs),
-
-    /// Translate a source file into Dioxus code.
-    #[clap(name = "translate")]
-    Translate(translate::Translate),
+    /// Create a new Dioxus project.
+    #[clap(name = "new")]
+    New(create::Create),
 
-    /// Build, watch & serve the Dioxus project and all of its assets.
+    /// Build, watch, and serve the project.
     #[clap(name = "serve")]
     Serve(serve::ServeArgs),
 
-    /// Create a new project for Dioxus.
-    #[clap(name = "new")]
-    New(create::Create),
+    /// Bundle the Dioxus app into a shippable object.
+    #[clap(name = "bundle")]
+    Bundle(bundle::Bundle),
+
+    /// Build the Dioxus project and all of its assets.
+    #[clap(name = "build")]
+    Build(CommandWithPlatformOverrides<build::BuildArgs>),
+
+    /// Run the project without any hotreloading.
+    #[clap(name = "run")]
+    Run(run::RunArgs),
 
     /// Init a new project for Dioxus in the current directory (by default).
     /// Will attempt to keep your project in a good state.
@@ -70,9 +79,9 @@ pub(crate) enum Commands {
     #[clap(name = "clean")]
     Clean(clean::Clean),
 
-    /// Bundle the Dioxus app into a shippable object.
-    #[clap(name = "bundle")]
-    Bundle(bundle::Bundle),
+    /// Translate a source file into Dioxus code.
+    #[clap(name = "translate")]
+    Translate(translate::Translate),
 
     /// Automatically format RSX.
     #[clap(name = "fmt")]
@@ -82,15 +91,23 @@ pub(crate) enum Commands {
     #[clap(name = "check")]
     Check(check::Check),
 
-    /// Run the project without any hotreloading
-    #[clap(name = "run")]
-    Run(run::RunArgs),
-
     /// Dioxus config file controls.
     #[clap(subcommand)]
     #[clap(name = "config")]
     Config(config::Config),
 
+    /// Update the Dioxus CLI to the latest version.
+    #[clap(name = "self-update")]
+    SelfUpdate(update::SelfUpdate),
+
+    /// Run a dioxus build tool. IE `build-assets`, etc
+    #[clap(name = "tools")]
+    #[clap(subcommand)]
+    Tools(BuildTools),
+}
+
+#[derive(Subcommand)]
+pub enum BuildTools {
     /// Build the assets for a specific target.
     #[clap(name = "assets")]
     BuildAssets(build_assets::BuildAssets),
@@ -110,15 +127,46 @@ impl Display for Commands {
             Commands::Check(_) => write!(f, "check"),
             Commands::Bundle(_) => write!(f, "bundle"),
             Commands::Run(_) => write!(f, "run"),
-            Commands::BuildAssets(_) => write!(f, "build_assets"),
+            Commands::SelfUpdate(_) => write!(f, "self-update"),
+            Commands::Tools(_) => write!(f, "tools"),
         }
     }
 }
 
-pub(crate) static VERSION: Lazy<String> = Lazy::new(|| {
+pub(crate) static VERSION: LazyLock<String> = LazyLock::new(|| {
     format!(
         "{} ({})",
         crate::dx_build_info::PKG_VERSION,
         crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
     )
 });
+
+/// Cargo's color style
+/// [source](https://github.com/crate-ci/clap-cargo/blob/master/src/style.rs)
+pub(crate) const CARGO_STYLING: Styles = Styles::styled()
+    .header(styles::HEADER)
+    .usage(styles::USAGE)
+    .literal(styles::LITERAL)
+    .placeholder(styles::PLACEHOLDER)
+    .error(styles::ERROR)
+    .valid(styles::VALID)
+    .invalid(styles::INVALID);
+
+pub mod styles {
+    use super::*;
+    pub(crate) const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
+    pub(crate) const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
+    pub(crate) const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
+    pub(crate) const PLACEHOLDER: Style = AnsiColor::Cyan.on_default();
+    pub(crate) const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
+
+    pub(crate) const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
+    pub(crate) const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
+
+    // extra styles for styling logs
+    // we can style stuff using the ansi sequences like: "hotpatched in {GLOW_STYLE}{}{GLOW_STYLE:#}ms"
+    pub(crate) const GLOW_STYLE: Style = AnsiColor::Yellow.on_default();
+    pub(crate) const NOTE_STYLE: Style = AnsiColor::Green.on_default();
+    pub(crate) const LINK_STYLE: Style = AnsiColor::Blue.on_default();
+    pub(crate) const ERROR_STYLE: Style = AnsiColor::Red.on_default();
+}

+ 169 - 0
packages/cli/src/cli/platform_override.rs

@@ -0,0 +1,169 @@
+#![allow(dead_code)]
+use clap::parser::ValueSource;
+use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand};
+
+/// Wraps a component with the subcommands `@server` and `@client` which will let you override the
+/// base arguments for the client and server instances.
+#[derive(Debug, Clone, Default)]
+pub struct CommandWithPlatformOverrides<T> {
+    /// The arguments that are shared between the client and server
+    pub shared: T,
+    /// The merged arguments for the server
+    pub server: Option<T>,
+    /// The merged arguments for the client
+    pub client: Option<T>,
+}
+
+impl<T> CommandWithPlatformOverrides<T> {
+    pub(crate) fn with_client_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O {
+        match &self.client {
+            Some(client) => f(client),
+            None => f(&self.shared),
+        }
+    }
+
+    pub(crate) fn with_server_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O {
+        match &self.server {
+            Some(server) => f(server),
+            None => f(&self.shared),
+        }
+    }
+}
+
+impl<T: CommandFactory + Args> Parser for CommandWithPlatformOverrides<T> {}
+
+impl<T: CommandFactory + Args> CommandFactory for CommandWithPlatformOverrides<T> {
+    fn command() -> clap::Command {
+        T::command()
+    }
+
+    fn command_for_update() -> clap::Command {
+        T::command_for_update()
+    }
+}
+
+impl<T> Args for CommandWithPlatformOverrides<T>
+where
+    T: Args,
+{
+    fn augment_args(cmd: clap::Command) -> clap::Command {
+        T::augment_args(cmd).defer(|cmd| {
+            PlatformOverrides::<Self>::augment_subcommands(cmd.disable_help_subcommand(true))
+        })
+    }
+
+    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
+        unimplemented!()
+    }
+}
+
+fn merge_matches<T: Args>(base: &ArgMatches, platform: &ArgMatches) -> Result<T, clap::Error> {
+    let mut base = T::from_arg_matches(base)?;
+
+    let mut platform = platform.clone();
+    let original_ids: Vec<_> = platform.ids().cloned().collect();
+    for arg_id in original_ids {
+        let arg_name = arg_id.as_str();
+        // Remove any default values from the platform matches
+        if platform.value_source(arg_name) == Some(ValueSource::DefaultValue) {
+            _ = platform.try_clear_id(arg_name);
+        }
+    }
+
+    // Then merge the stripped platform matches into the base matches
+    base.update_from_arg_matches(&platform)?;
+
+    Ok(base)
+}
+
+impl<T> FromArgMatches for CommandWithPlatformOverrides<T>
+where
+    T: Args,
+{
+    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
+        let mut client = None;
+        let mut server = None;
+        let mut subcommand = matches.subcommand();
+        while let Some((name, sub_matches)) = subcommand {
+            match name {
+                "@client" => client = Some(sub_matches),
+                "@server" => server = Some(sub_matches),
+                _ => {}
+            }
+            subcommand = sub_matches.subcommand();
+        }
+
+        let shared = T::from_arg_matches(matches)?;
+        let client = client
+            .map(|client| merge_matches::<T>(matches, client))
+            .transpose()?;
+        let server = server
+            .map(|server| merge_matches::<T>(matches, server))
+            .transpose()?;
+
+        Ok(Self {
+            shared,
+            server,
+            client,
+        })
+    }
+
+    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
+        unimplemented!()
+    }
+}
+
+#[derive(Debug, Subcommand, Clone)]
+#[command(subcommand_precedence_over_arg = true)]
+pub(crate) enum PlatformOverrides<T: Args> {
+    /// Specify the arguments for the client build
+    #[clap(name = "@client")]
+    Client(ChainedCommand<T, PlatformOverrides<T>>),
+
+    /// Specify the arguments for the server build
+    #[clap(name = "@server")]
+    Server(ChainedCommand<T, PlatformOverrides<T>>),
+}
+
+// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894
+//
+//
+/// `[Args]` wrapper to match `T` variants recursively in `U`.
+#[derive(Debug, Clone)]
+pub struct ChainedCommand<T, U> {
+    /// Specific Variant.
+    pub inner: T,
+
+    /// Enum containing `Self<T>` variants, in other words possible follow-up commands.
+    pub next: Option<Box<U>>,
+}
+
+impl<T, U> Args for ChainedCommand<T, U>
+where
+    T: Args,
+    U: Subcommand,
+{
+    fn augment_args(cmd: clap::Command) -> clap::Command {
+        // We use the special `defer` method which lets us recursively call `augment_args` on the inner command
+        // and thus `from_arg_matches`
+        T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true)))
+    }
+
+    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
+        unimplemented!()
+    }
+}
+
+impl<T, U> FromArgMatches for ChainedCommand<T, U>
+where
+    T: Args,
+    U: Subcommand,
+{
+    fn from_arg_matches(_: &ArgMatches) -> Result<Self, clap::Error> {
+        unimplemented!()
+    }
+
+    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
+        unimplemented!()
+    }
+}

+ 46 - 11
packages/cli/src/cli/run.rs

@@ -1,13 +1,16 @@
 use super::*;
 use crate::{
     serve::{AppServer, ServeUpdate, WebServer},
-    BuilderUpdate, Platform, Result,
+    BuilderUpdate, Error, Platform, Result,
 };
 use dioxus_dx_wire_format::BuildStage;
 
 /// Run the project with the given arguments
 ///
 /// This is a shorthand for `dx serve` with interactive mode and hot-reload disabled.
+///
+/// Unlike `dx serve`, errors during build and run will cascade out as an error, rather than being
+/// handled by the TUI, making it more suitable for scripting, automation, or CI/CD pipelines.
 #[derive(Clone, Debug, Parser)]
 pub(crate) struct RunArgs {
     /// Information about the target to build
@@ -36,20 +39,20 @@ impl RunArgs {
             };
 
             match msg {
-                // 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::BuilderUpdate { id, update } => {
                     let platform = builder.get_build(id).unwrap().build.platform;
 
                     // And then update the websocketed clients with the new build status in case they want it
                     devserver.new_build_update(&update).await;
 
+                    // Finally, we also want to update the builder with the new update
+                    builder.new_build_update(&update, &devserver).await;
+
                     // And then open the app if it's ready
                     match update {
                         BuilderUpdate::BuildReady { bundle } => {
                             _ = builder
-                                .open(bundle, &mut devserver)
+                                .open(&bundle, &mut devserver)
                                 .await
                                 .inspect_err(|e| tracing::error!("Failed to open app: {}", e));
 
@@ -72,7 +75,9 @@ impl RunArgs {
                                 total,
                                 krate,
                             } => {
-                                tracing::info!("[{platform}] Compiling {krate} ({current}/{total})",)
+                                tracing::debug!(
+                                    "[{platform}] ({current}/{total}) Compiling {krate} ",
+                                )
                             }
                             BuildStage::RunningBindgen => {
                                 tracing::info!("[{platform}] Running WASM bindgen")
@@ -82,7 +87,7 @@ impl RunArgs {
                                 tracing::info!("[{platform}] Optimizing WASM with `wasm-opt`")
                             }
                             BuildStage::Linking => tracing::info!("Linking app"),
-                            BuildStage::Hotpatching => todo!(),
+                            BuildStage::Hotpatching => {}
                             BuildStage::CopyingAssets {
                                 current,
                                 total,
@@ -96,17 +101,32 @@ impl RunArgs {
                                 tracing::info!("[{platform}] Running Gradle")
                             }
                             BuildStage::Success => {}
-                            BuildStage::Failed => {}
-                            BuildStage::Aborted => {}
                             BuildStage::Restarting => {}
                             BuildStage::CompressingAssets => {}
+                            BuildStage::ExtractingAssets => {}
+                            BuildStage::Prerendering => {
+                                tracing::info!("[{platform}] Prerendering app")
+                            }
+                            BuildStage::Failed => {
+                                tracing::error!("[{platform}] Build failed");
+                                return Err(Error::Cargo(format!(
+                                    "Build failed for platform: {platform}"
+                                )));
+                            }
+                            BuildStage::Aborted => {
+                                tracing::error!("[{platform}] Build aborted");
+                                return Err(Error::Cargo(format!(
+                                    "Build aborted for platform: {platform}"
+                                )));
+                            }
                             _ => {}
                         },
                         BuilderUpdate::CompilerMessage { message } => {
                             print!("{}", message);
                         }
                         BuilderUpdate::BuildFailed { err } => {
-                            tracing::error!("Build failed: {:#?}", err);
+                            tracing::error!("Build failed: {}", err);
+                            return Err(err);
                         }
                         BuilderUpdate::StdoutReceived { msg } => {
                             tracing::info!("[{platform}] {msg}");
@@ -119,13 +139,28 @@ impl RunArgs {
                                 tracing::error!(
                                     "Application [{platform}] exited with error: {status}"
                                 );
+                                return Err(Error::Runtime(format!(
+                                    "Application [{platform}] exited with error: {status}"
+                                )));
                             }
 
                             break;
                         }
+                        BuilderUpdate::ProcessWaitFailed { err } => {
+                            return Err(err.into());
+                        }
                     }
                 }
-                _ => {}
+                ServeUpdate::Exit { .. } => break,
+                ServeUpdate::NewConnection { .. } => {}
+                ServeUpdate::WsMessage { .. } => {}
+                ServeUpdate::FilesChanged { .. } => {}
+                ServeUpdate::OpenApp => {}
+                ServeUpdate::RequestRebuild => {}
+                ServeUpdate::ToggleShouldRebuild => {}
+                ServeUpdate::OpenDebugger { .. } => {}
+                ServeUpdate::Redraw => {}
+                ServeUpdate::TracingLog { .. } => {}
             }
         }
 

+ 21 - 14
packages/cli/src/cli/serve.rs

@@ -1,32 +1,27 @@
 use super::*;
 use crate::{AddressArguments, BuildArgs, TraceController};
 use futures_util::FutureExt;
-use once_cell::sync::OnceCell;
+use std::sync::OnceLock;
 use std::{backtrace::Backtrace, panic::AssertUnwindSafe};
 
 /// Serve the project
 ///
 /// `dx serve` takes cargo args by default, except with a required `--platform` arg:
 ///
-/// ```
+/// ```sh
 /// dx serve --example blah --target blah --platform android
 /// ```
 ///
 /// A simple serve:
-/// ```
+/// ```sh
 /// dx serve --platform web
 /// ```
 ///
-/// A serve with customized arguments:
-///
-/// ```
-/// ```
-///
 /// As of dioxus 0.7, `dx serve` allows independent customization of the client and server builds,
 /// allowing workspaces and removing any "magic" done to support ergonomic fullstack serving with
 /// an plain `dx serve`. These require specifying more arguments like features since they won't be autodetected.
 ///
-/// ```
+/// ```sh
 /// dx serve \
 ///     client --package frontend \
 ///     server --package backend
@@ -55,10 +50,6 @@ pub(crate) struct ServeArgs {
     #[clap(long)]
     pub(crate) cross_origin_policy: bool,
 
-    /// Additional arguments to pass to the executable
-    #[clap(long)]
-    pub(crate) args: Vec<String>,
-
     /// Sets the interval in seconds that the CLI will poll for file changes on WSL.
     #[clap(long, default_missing_value = "2")]
     pub(crate) wsl_file_poll_interval: Option<u16>,
@@ -81,8 +72,24 @@ pub(crate) struct ServeArgs {
     #[clap(long)]
     pub(crate) force_sequential: bool,
 
+    /// Exit the CLI after running into an error. This is mainly used to test hot patching internally
+    #[clap(long)]
+    #[clap(hide = true)]
+    pub(crate) exit_on_error: bool,
+
+    /// Platform-specific arguments for the build
+    #[clap(flatten)]
+    pub(crate) platform_args: CommandWithPlatformOverrides<PlatformServeArgs>,
+}
+
+#[derive(Clone, Debug, Default, Parser)]
+pub(crate) struct PlatformServeArgs {
     #[clap(flatten)]
     pub(crate) targets: BuildArgs,
+
+    /// Additional arguments to pass to the executable
+    #[clap(long, default_value = "")]
+    pub(crate) args: String,
 }
 
 impl ServeArgs {
@@ -102,7 +109,7 @@ impl ServeArgs {
             line: u32,
             column: u32,
         }
-        static BACKTRACE: OnceCell<(Backtrace, Option<SavedLocation>)> = OnceCell::new();
+        static BACKTRACE: OnceLock<(Backtrace, Option<SavedLocation>)> = OnceLock::new();
 
         // We *don't* want printing here, since it'll break the tui and log ordering.
         //

+ 24 - 92
packages/cli/src/cli/target.rs

@@ -1,50 +1,51 @@
 use crate::cli::*;
 use crate::Platform;
-use clap::{ArgMatches, Args, FromArgMatches, Subcommand};
 use target_lexicon::Triple;
 
+const HELP_HEADING: &str = "Target Options";
+
 /// A single target to build for
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub(crate) struct TargetArgs {
     /// Build platform: support Web & Desktop [default: "default_platform"]
-    #[clap(long, value_enum)]
+    #[clap(long, value_enum, help_heading = HELP_HEADING)]
     pub(crate) platform: Option<Platform>,
 
     /// Build in release mode [default: false]
-    #[clap(long, short)]
+    #[clap(long, short, help_heading = HELP_HEADING)]
     #[serde(default)]
     pub(crate) release: bool,
 
     /// The package to build
-    #[clap(short, long)]
+    #[clap(short, long, help_heading = HELP_HEADING)]
     pub(crate) package: Option<String>,
 
     /// Build a specific binary [default: ""]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) bin: Option<String>,
 
     /// Build a specific example [default: ""]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) example: Option<String>,
 
     /// Build the app with custom a profile
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) profile: Option<String>,
 
     /// Space separated list of features to activate
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) features: Vec<String>,
 
     /// Don't include the default features in the build
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) no_default_features: bool,
 
     /// Include all features in the build
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) all_features: bool,
 
     /// Rustc platform triple
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) target: Option<Triple>,
 
     /// Extra arguments passed to `cargo`
@@ -53,7 +54,7 @@ pub(crate) struct TargetArgs {
     ///
     /// This can include stuff like, "--locked", "--frozen", etc. Note that `dx` sets many of these
     /// args directly from other args in this command.
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) cargo_args: Option<String>,
 
     /// Extra arguments passed to `rustc`. This can be used to customize the linker, or other flags.
@@ -63,105 +64,36 @@ pub(crate) struct TargetArgs {
     ///
     /// cargo rustc -- -Clink-arg=-Wl,-blah
     ///
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) rustc_args: Option<String>,
 
     /// Skip collecting assets from dependencies [default: false]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     #[serde(default)]
     pub(crate) skip_assets: bool,
 
     /// 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)]
+    #[clap(long, default_value_t = true, help_heading = HELP_HEADING)]
     pub(crate) inject_loading_scripts: bool,
 
     /// Experimental: Bundle split the wasm binary into multiple chunks based on `#[wasm_split]` annotations [default: false]
-    #[clap(long, default_value_t = false)]
+    #[clap(long, default_value_t = false, help_heading = HELP_HEADING)]
     pub(crate) wasm_split: bool,
 
     /// Generate debug symbols for the wasm binary [default: true]
     ///
     /// This will make the binary larger and take longer to compile, but will allow you to debug the
     /// wasm binary
-    #[clap(long, default_value_t = true)]
+    #[clap(long, default_value_t = true, help_heading = HELP_HEADING)]
     pub(crate) debug_symbols: bool,
 
     /// 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<bool>,
-}
-
-/// Chain together multiple target commands
-#[derive(Debug, Subcommand, Clone)]
-#[command(subcommand_precedence_over_arg = true)]
-pub(crate) enum TargetCmd {
-    /// Specify the arguments for the client build
-    #[clap(name = "client")]
-    Client(ChainedCommand<TargetArgs, TargetCmd>),
-
-    /// Specify the arguments for the server build
-    #[clap(name = "server")]
-    Server(ChainedCommand<TargetArgs, TargetCmd>),
-}
-
-// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894
-//
-//
-/// `[Args]` wrapper to match `T` variants recursively in `U`.
-#[derive(Debug, Clone)]
-pub struct ChainedCommand<T, U> {
-    /// Specific Variant.
-    pub inner: T,
-
-    /// Enum containing `Self<T>` variants, in other words possible follow-up commands.
-    pub next: Option<Box<U>>,
-}
-
-impl<T, U> Args for ChainedCommand<T, U>
-where
-    T: Args,
-    U: Subcommand,
-{
-    fn augment_args(cmd: clap::Command) -> clap::Command {
-        // We use the special `defer` method which lets us recursively call `augment_args` on the inner command
-        // and thus `from_arg_matches`
-        T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true)))
-    }
-
-    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
-        unimplemented!()
-    }
-}
+    #[clap(long, default_value_t = false, help_heading = HELP_HEADING)]
+    pub(crate) device: bool,
 
-impl<T, U> FromArgMatches for ChainedCommand<T, U>
-where
-    T: Args,
-    U: Subcommand,
-{
-    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
-        // Parse the first command before we try to parse the next one.
-        let inner = T::from_arg_matches(matches)?;
-
-        // Try to parse the remainder of the command as a subcommand.
-        let next = match matches.subcommand() {
-            // Subcommand skips into the matched .subcommand, hence we need to pass *outer* matches, ignoring the inner matches
-            // (which in the average case should only match enumerated T)
-            //
-            // Here, we might want to eventually enable arbitrary names of subcommands if they're prefixed
-            // with a prefix like "@" ie `dx serve @dog-app/backend --args @dog-app/frontend --args`
-            //
-            // we are done, since sub-sub commands are matched in U::
-            Some(_) => Some(Box::new(U::from_arg_matches(matches)?)),
-
-            // no subcommand matched, we are done
-            None => None,
-        };
-
-        Ok(Self { inner, next })
-    }
-
-    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
-        unimplemented!()
-    }
+    /// The base path the build will fetch assets relative to. This will override the
+    /// base path set in the `dioxus` config.
+    #[clap(long, help_heading = HELP_HEADING)]
+    pub(crate) base_path: Option<String>,
 }

+ 247 - 0
packages/cli/src/cli/update.rs

@@ -0,0 +1,247 @@
+use super::*;
+use crate::{Result, Workspace};
+use anyhow::Context;
+use itertools::Itertools;
+use self_update::cargo_crate_version;
+
+/// Run the project with the given arguments
+///
+/// This is a shorthand for `dx serve` with interactive mode and hot-reload disabled.
+#[derive(Clone, Debug, Parser)]
+pub(crate) struct SelfUpdate {
+    /// Use the latest nightly build.
+    #[clap(long, default_value = "false")]
+    pub nightly: bool,
+
+    /// Specify a version to install.
+    #[clap(long)]
+    pub version: Option<String>,
+
+    /// Install the update.
+    #[clap(long, default_value = "true")]
+    pub install: bool,
+
+    /// List available versions.
+    #[clap(long, default_value = "false")]
+    pub list: bool,
+
+    /// Force the update even if the current version is up to date.
+    #[clap(long, default_value = "false")]
+    pub force: bool,
+}
+
+impl SelfUpdate {
+    pub async fn self_update(self) -> Result<StructuredOutput> {
+        tokio::task::spawn_blocking(move || {
+            let start = std::time::Instant::now();
+            if self.list {
+                let res = self_update::backends::github::Update::configure()
+                    .repo_owner("dioxuslabs")
+                    .repo_name("dioxus")
+                    .bin_name("dx")
+                    .current_version(cargo_crate_version!())
+                    .build()
+                    .unwrap()
+                    .get_latest_releases(cargo_crate_version!())
+                    .context("Failed to fetch latest version")?;
+
+                if res.is_empty() {
+                    tracing::info!("Your version {} is up to date!", cargo_crate_version!());
+                } else {
+                    tracing::info!("Your version {} is out of date!", cargo_crate_version!());
+                    tracing::info!(
+                        "Available versions: [{}]",
+                        res.iter()
+                            .map(|r| r.version.clone())
+                            .collect::<Vec<_>>()
+                            .join(", ")
+                    );
+                }
+
+                return Ok(StructuredOutput::Success);
+            }
+
+            let repo = self_update::backends::github::Update::configure()
+                .repo_owner("dioxuslabs")
+                .repo_name("dioxus")
+                .bin_name("dx")
+                .current_version(cargo_crate_version!())
+                .build()
+                .unwrap();
+
+            let force = self.force || self.version.is_some();
+            let latest = match self.version {
+                Some(version) => repo
+                    .get_release_version(&version)
+                    .context("Failed to fetch release by tag")?,
+                None => repo
+                    .get_latest_release()
+                    .context("Failed to fetch latest version")?,
+            };
+
+            if latest.version == cargo_crate_version!() && !force {
+                tracing::info!("Your version {} is up to date!", cargo_crate_version!());
+                return Ok(StructuredOutput::Success);
+            }
+
+            tracing::info!("Your version is out of date!");
+            tracing::info!("- Yours:  {}", cargo_crate_version!());
+            tracing::info!("- Latest: {}", latest.version);
+
+            let cur_arch = if cfg!(target_arch = "x86_64") {
+                "x86_64"
+            } else if cfg!(target_arch = "aarch64") {
+                "aarch64"
+            } else {
+                return Err(Error::Unique("Unsupported architecture".to_string()));
+            };
+
+            let cur_os = if cfg!(target_os = "windows") {
+                "windows"
+            } else if cfg!(target_os = "linux") {
+                "linux"
+            } else if cfg!(target_os = "macos") {
+                "darwin"
+            } else {
+                return Err(Error::Unique("Unsupported OS".to_string()));
+            };
+
+            let zip_ext = "zip";
+
+            tracing::debug!("Available assets: {:?}", latest.assets);
+
+            let asset = latest
+                .assets
+                .iter()
+                .find(|a| {
+                    a.name.contains(cur_os)
+                        && a.name.contains(cur_arch)
+                        && a.name.ends_with(zip_ext)
+                })
+                .ok_or_else(|| Error::Unique("No suitable release found found".to_string()))?;
+
+            let install_dir = Workspace::dioxus_home_dir().join("self-update");
+            std::fs::create_dir_all(&install_dir).context("Failed to create install directory")?;
+
+            tracing::info!("Downloading update from Github");
+            tracing::debug!("Download URL: {}", asset.download_url);
+            let body = latest.body.unwrap_or_default();
+            let brief = vec![
+                latest.name.to_string(),
+                "".to_string(),
+                latest.date.to_string(),
+                asset.download_url.to_string(),
+                "".to_string(),
+            ]
+            .into_iter()
+            .chain(body.lines().map(ToString::to_string).take(7))
+            .chain(std::iter::once(" ...".to_string()))
+            .map(|line| format!("                | {line}"))
+            .join("\n");
+
+            tracing::info!("{}", brief.trim());
+
+            let archive_path = install_dir.join(&asset.name);
+            _ = std::fs::remove_file(&archive_path).ok();
+            let archive_file = std::fs::File::create(&archive_path)?;
+            let download_url = asset.download_url.clone();
+            self_update::Download::from_url(&download_url)
+                .set_header(
+                    hyper::http::header::ACCEPT,
+                    "application/octet-stream".parse().unwrap(),
+                )
+                .download_to(archive_file)
+                .context("Failed to download update")?;
+
+            let install_dir = install_dir.join("dx");
+            _ = std::fs::remove_dir_all(&install_dir);
+            self_update::Extract::from_source(&archive_path)
+                .extract_into(&install_dir)
+                .context("Failed to extract update")?;
+
+            let executable = install_dir.join("dx");
+            if !executable.exists() {
+                return Err(Error::Unique(format!(
+                    "Executable not found in {}",
+                    install_dir.display()
+                )));
+            }
+
+            tracing::info!(
+                "Successfully downloaded update in {}ms! 👍",
+                start.elapsed().as_millis()
+            );
+
+            if self.install {
+                tracing::info!(
+                    "Installing dx v{} to {}",
+                    latest.version,
+                    std::env::current_exe()?.display()
+                );
+
+                if !self.force {
+                    tracing::warn!("Continue? (y/n)");
+                    print!("                > ");
+                    std::io::stdout()
+                        .flush()
+                        .context("Failed to flush stdout")?;
+                    let mut input = String::new();
+                    std::io::stdin()
+                        .read_line(&mut input)
+                        .context("Failed to read input")?;
+                    if !input.trim().to_ascii_lowercase().starts_with('y') {
+                        tracing::info!("Aborting update");
+                        return Ok(StructuredOutput::Success);
+                    }
+                }
+
+                self_update::self_replace::self_replace(executable)?;
+                let time_taken = start.elapsed().as_millis();
+                tracing::info!("Done in {} ms! 💫", time_taken)
+            } else {
+                tracing::info!("Update downloaded to {}", install_dir.display());
+                tracing::info!("Run `dx self-update --install` to install the update");
+            }
+
+            Ok(StructuredOutput::Success)
+        })
+        .await
+        .context("Failed to run self-update")?
+    }
+}
+
+/// Check against the github release list to see if the currently released `dx` version is
+/// more up-to-date than our own.
+///
+/// We only toss out this warning once and then save to the settings file to ignore this version
+/// in the future.
+pub fn log_if_cli_could_update() {
+    tokio::task::spawn_blocking(|| {
+        let release = self_update::backends::github::Update::configure()
+            .repo_owner("dioxuslabs")
+            .repo_name("dioxus")
+            .bin_name("dx")
+            .current_version(cargo_crate_version!())
+            .build()
+            .unwrap()
+            .get_latest_release();
+
+        if let Ok(release) = release {
+            let old = krates::semver::Version::parse(cargo_crate_version!());
+            let new = krates::semver::Version::parse(&release.version);
+
+            if let (Ok(old), Ok(new)) = (old, new) {
+                if old < new {
+                    _ = crate::CliSettings::modify_settings(|f| {
+                        let ignored = f.ignore_version_update.as_deref().unwrap_or_default();
+                        if release.version != ignored {
+                            use crate::styles::GLOW_STYLE;
+                            tracing::warn!("A new dx version is available: {new}! Run {GLOW_STYLE}dx self-update{GLOW_STYLE:#} to update.");
+                            f.ignore_version_update = Some(new.to_string());
+                        }
+                    });
+                }
+            }
+        }
+    });
+}

+ 13 - 1
packages/cli/src/cli/verbosity.rs

@@ -1,6 +1,6 @@
 use clap::Parser;
 
-#[derive(Parser, Clone, Copy, Debug)]
+#[derive(Parser, Clone, Copy, Debug, Default)]
 pub struct Verbosity {
     /// Use verbose output [default: false]
     #[clap(long, global = true)]
@@ -13,4 +13,16 @@ pub struct Verbosity {
     /// Output logs in JSON format
     #[clap(long, global = true)]
     pub(crate) json_output: bool,
+
+    /// Assert that `Cargo.lock` will remain unchanged
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) locked: bool,
+
+    /// Run without accessing the network
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) offline: bool,
+
+    /// Equivalent to specifying both --locked and --offline
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) frozen: bool,
 }

+ 23 - 5
packages/cli/src/config/app.rs

@@ -3,11 +3,6 @@ use std::path::PathBuf;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub(crate) struct ApplicationConfig {
-    pub(crate) asset_dir: Option<PathBuf>,
-
-    #[serde(default)]
-    pub(crate) sub_package: Option<String>,
-
     #[serde(default)]
     pub(crate) out_dir: Option<PathBuf>,
 
@@ -16,4 +11,27 @@ pub(crate) struct ApplicationConfig {
 
     #[serde(default)]
     pub(crate) tailwind_output: Option<PathBuf>,
+
+    /// Use this file for the info.plist associated with the iOS app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) ios_info_plist: Option<PathBuf>,
+
+    /// Use this file for the info.plist associated with the macOS app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) macos_info_plist: Option<PathBuf>,
+
+    /// Use this file for the entitlements.plist associated with the iOS app.
+    #[serde(default)]
+    pub(crate) ios_entitlements: Option<PathBuf>,
+
+    /// Use this file for the entitlements.plist associated with the macOS app.
+    #[serde(default)]
+    pub(crate) macos_entitlements: Option<PathBuf>,
+
+    /// Use this file for the AndroidManifest.xml associated with the Android app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) android_manifest: Option<PathBuf>,
 }

+ 1 - 1
packages/cli/src/config/bundle.rs

@@ -80,7 +80,7 @@ pub(crate) struct WixSettings {
     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.
+    /// Because a valid version is required for MSI installer, it will be derived from [`tauri_bundler::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.

+ 0 - 5
packages/cli/src/config/desktop.rs

@@ -1,5 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-/// Represents configuration items for the desktop platform.
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
-pub(crate) struct DesktopConfig {}

+ 6 - 7
packages/cli/src/config/dioxus_config.rs

@@ -8,9 +8,6 @@ pub(crate) struct DioxusConfig {
     #[serde(default)]
     pub(crate) web: WebConfig,
 
-    #[serde(default)]
-    pub(crate) desktop: DesktopConfig,
-
     #[serde(default)]
     pub(crate) bundle: BundleConfig,
 }
@@ -19,11 +16,14 @@ impl Default for DioxusConfig {
     fn default() -> Self {
         Self {
             application: ApplicationConfig {
-                asset_dir: None,
-                sub_package: None,
                 out_dir: None,
                 tailwind_input: None,
                 tailwind_output: None,
+                ios_info_plist: None,
+                android_manifest: None,
+                macos_info_plist: None,
+                ios_entitlements: None,
+                macos_entitlements: None,
             },
             web: WebConfig {
                 app: WebAppConfig {
@@ -46,10 +46,9 @@ impl Default for DioxusConfig {
                     key_path: None,
                     cert_path: None,
                 },
-                pre_compress: true,
+                pre_compress: false,
                 wasm_opt: Default::default(),
             },
-            desktop: DesktopConfig::default(),
             bundle: BundleConfig::default(),
         }
     }

+ 0 - 2
packages/cli/src/config/mod.rs

@@ -1,13 +1,11 @@
 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::*;

+ 10 - 0
packages/cli/src/config/web.rs

@@ -62,6 +62,16 @@ pub(crate) struct WasmOptConfig {
     /// Enable memory packing
     #[serde(default = "false_bool")]
     pub(crate) memory_packing: bool,
+
+    /// Extra arguments to pass to wasm-opt
+    ///
+    /// For example, to enable simd, you can set this to `["--enable-simd"]`.
+    ///
+    /// You can also disable features by prefixing them with `--disable-`, e.g. `["--disable-bulk-memory"]`.
+    ///
+    /// Currently only --enable and --disable flags are supported.
+    #[serde(default)]
+    pub(crate) extra_features: Vec<String>,
 }
 
 /// The wasm-opt level to use for release web builds [default: Z]

+ 0 - 10
packages/cli/src/devcfg.rs

@@ -1,15 +1,5 @@
 //! Configuration of the CLI at runtime to enable certain experimental features.
 
-use std::path::Path;
-
-/// Should we cache the dependency library?
-///
-/// When the `DIOXUS_CACHE_DEP_LIB` environment variable is set, we will cache the dependency library
-/// built from the target's dependencies.
-pub(crate) fn should_cache_dep_lib(lib: &Path) -> bool {
-    std::env::var("DIOXUS_CACHE_DEP_LIB").is_ok() && lib.exists()
-}
-
 /// Should we force the entropy to be used on the main exe?
 ///
 /// This is used to verify that binaries are copied with different names such that they don't collide

+ 10 - 4
packages/cli/src/error.rs

@@ -27,21 +27,27 @@ pub(crate) enum Error {
     #[error("Invalid proxy URL: {0}")]
     InvalidProxy(#[from] hyper::http::uri::InvalidUri),
 
-    #[error("Failed to establish proxy: {0}")]
+    #[error("Establishing proxy: {0}")]
     ProxySetup(String),
 
-    #[error("Failed to bundle project: {0}")]
+    #[error("Bundling project: {0}")]
     BundleFailed(#[from] tauri_bundler::Error),
 
-    #[error("Failed to perform hotpatch: {0}")]
+    #[error("Performing hotpatch: {0}")]
     PatchingFailed(#[from] crate::build::PatchError),
 
+    #[error("Reading object file: {0}")]
+    ObjectReadFailed(#[from] object::Error),
+
     #[error("{0}")]
     CapturedPanic(String),
 
-    #[error("Failed to render template: {0}")]
+    #[error("Rendering template error: {0}")]
     TemplateParse(#[from] handlebars::RenderError),
 
+    #[error("Network connectivity error: {0}")]
+    Network(String),
+
     #[error(transparent)]
     Other(#[from] anyhow::Error),
 }

+ 12 - 7
packages/cli/src/logging.rs

@@ -15,10 +15,10 @@
 //! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode.
 
 use crate::{serve::ServeUpdate, Cli, Commands, Platform as TargetPlatform, Verbosity};
-use cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage};
+use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel};
 use clap::Parser;
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
-use once_cell::sync::OnceCell;
+use std::sync::OnceLock;
 use std::{
     collections::HashMap,
     env,
@@ -47,8 +47,8 @@ const LOG_FILE_NAME: &str = "dx.log";
 const DX_SRC_FLAG: &str = "dx_src";
 
 static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
-static TUI_TX: OnceCell<UnboundedSender<TraceMsg>> = OnceCell::new();
-pub static VERBOSITY: OnceCell<Verbosity> = OnceCell::new();
+static TUI_TX: OnceLock<UnboundedSender<TraceMsg>> = OnceLock::new();
+pub static VERBOSITY: OnceLock<Verbosity> = OnceLock::new();
 
 pub(crate) struct TraceController {
     pub(crate) tui_rx: UnboundedReceiver<TraceMsg>,
@@ -82,6 +82,11 @@ impl TraceController {
             ))
         };
 
+        #[cfg(feature = "tokio-console")]
+        let filter = filter
+            .add_directive("tokio=trace".parse().unwrap())
+            .add_directive("runtime=trace".parse().unwrap());
+
         let json_filter = tracing_subscriber::filter::filter_fn(move |meta| {
             if meta.fields().len() == 1 && meta.fields().iter().next().unwrap().name() == "json" {
                 return args.verbosity.json_output;
@@ -349,7 +354,7 @@ pub struct TraceMsg {
 #[derive(Clone, PartialEq)]
 #[allow(clippy::large_enum_variant)]
 pub enum TraceContent {
-    Cargo(CompilerMessage),
+    Cargo(Diagnostic),
     Text(String),
 }
 
@@ -366,9 +371,9 @@ impl TraceMsg {
     /// 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 {
+    pub fn cargo(content: Diagnostic) -> Self {
         Self {
-            level: match content.message.level {
+            level: match content.level {
                 DiagnosticLevel::Ice => Level::ERROR,
                 DiagnosticLevel::Error => Level::ERROR,
                 DiagnosticLevel::FailureNote => Level::ERROR,

部分文件因为文件数量过多而无法显示