├── .github ├── assets │ └── tinyweb-youtube.jpg ├── notes │ └── runtime.md └── workflows │ └── tests.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── callbacks │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ ├── public │ │ └── index.html │ └── src │ │ └── lib.rs ├── dom │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ ├── public │ │ └── index.html │ └── src │ │ └── lib.rs ├── features │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ ├── public │ │ └── index.html │ └── src │ │ ├── keycodes.rs │ │ └── lib.rs └── minimal │ ├── Cargo.toml │ └── src │ └── lib.rs └── src ├── js ├── main.js └── main.test.js └── rust ├── Cargo.toml ├── src ├── allocations.rs ├── callbacks.rs ├── element.rs ├── invoke.rs ├── lib.rs ├── router.rs ├── runtime.rs └── signals.rs └── tests └── mod.rs /.github/assets/tinyweb-youtube.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveDuo/tinyweb/45266dc878734e7d94e74d53472b994165828875/.github/assets/tinyweb-youtube.jpg -------------------------------------------------------------------------------- /.github/notes/runtime.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Runtime flow 4 | 5 | ```js 6 | // 1. Register a callback and invoke `fetch` that triggers the callback when is finishes 7 | [Log] create_async_callback future_id=0 -> [Log] create_callback id=0 && [Log] js_invoke `fetch` id=0 8 | 9 | // 2. Use the `block_on` method and `await` the future inside 10 | // When the future is awaited it calls `poll` function that sets `FutureState` to `Pending(waker)` 11 | [Log] runtime block on -> [Log] future poll -> [Log] poll future pending 12 | 13 | // 3. When the `fetch` callback is triggered, schedule a `setTimeout(0)` callback that calls future poll 14 | [Log] handle_callback id=0 -> [Log] waker wake -> [Log] create_callback id=2 && [Log] js_invoke `setTimeout(0)` id=0 15 | 16 | // 4. When the `setTimeout(0)` callback is triggered, resolve future to `Poll::Ready(T)` 17 | [Log] handle_callback id=0 -> [Log] future poll -> [Log] poll future completed 18 | ``` 19 | 20 | 21 | ### Implementation quirks 22 | 23 | 1. Updating `FutureState` in 2 different places 24 | - Notes: It's updated in `create_async_callback` and in the `Future` trait impl 25 | - Explanation: `create_async_callback` has the `result` value and `Future` has access to the concrete `self` type 26 | 27 | 2. Calling `poll` in `wake_fn` through a Javascript callback instead of directly calling 28 | - Notes: The `wake_fn` function schedules a callback with `setTimeout(0)` that does `Runtime::poll(&future)` 29 | - Explanation: `Runtime::poll` has a mutable borrow that still holds when `wake_fn` tries to borrow again 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Tests 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | 8 | env: 9 | RUST_BACKTRACE: short 10 | SKIP_GUEST_BUILD: 1 11 | CARGO_INCREMENTAL: 0 12 | CARGO_NET_RETRY: 10 13 | CI: 1 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | 21 | # setup geckodriver 22 | - uses: browser-actions/setup-geckodriver@latest 23 | 24 | # setup git 25 | - uses: actions/checkout@v3 26 | - uses: Swatinem/rust-cache@v2 27 | 28 | # setup rust 29 | - run: export PATH=~/.cargo/bin:/usr/local/bin/:$PATH 30 | - run: rustup target add wasm32-unknown-unknown 31 | 32 | # run rust tests 33 | - run: cargo test 34 | 35 | # run js tests 36 | - run: node src/js/main.test.js 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.4.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 25 | 26 | [[package]] 27 | name = "backtrace" 28 | version = "0.3.74" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 31 | dependencies = [ 32 | "addr2line", 33 | "cfg-if", 34 | "libc", 35 | "miniz_oxide", 36 | "object", 37 | "rustc-demangle", 38 | "windows-targets", 39 | ] 40 | 41 | [[package]] 42 | name = "base64" 43 | version = "0.21.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.22.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "2.6.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 58 | 59 | [[package]] 60 | name = "bytes" 61 | version = "1.8.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 64 | 65 | [[package]] 66 | name = "callbacks" 67 | version = "0.1.0" 68 | dependencies = [ 69 | "tinyweb", 70 | ] 71 | 72 | [[package]] 73 | name = "cc" 74 | version = "1.1.37" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" 77 | dependencies = [ 78 | "shlex", 79 | ] 80 | 81 | [[package]] 82 | name = "cfg-if" 83 | version = "1.0.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 86 | 87 | [[package]] 88 | name = "cookie" 89 | version = "0.16.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 92 | dependencies = [ 93 | "time", 94 | "version_check", 95 | ] 96 | 97 | [[package]] 98 | name = "cookie" 99 | version = "0.18.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 102 | dependencies = [ 103 | "percent-encoding", 104 | "time", 105 | "version_check", 106 | ] 107 | 108 | [[package]] 109 | name = "core-foundation" 110 | version = "0.9.4" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 113 | dependencies = [ 114 | "core-foundation-sys", 115 | "libc", 116 | ] 117 | 118 | [[package]] 119 | name = "core-foundation-sys" 120 | version = "0.8.7" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 123 | 124 | [[package]] 125 | name = "deranged" 126 | version = "0.3.11" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 129 | dependencies = [ 130 | "powerfmt", 131 | ] 132 | 133 | [[package]] 134 | name = "displaydoc" 135 | version = "0.2.5" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 138 | dependencies = [ 139 | "proc-macro2", 140 | "quote", 141 | "syn", 142 | ] 143 | 144 | [[package]] 145 | name = "dom" 146 | version = "0.1.0" 147 | dependencies = [ 148 | "tinyweb", 149 | ] 150 | 151 | [[package]] 152 | name = "errno" 153 | version = "0.3.9" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 156 | dependencies = [ 157 | "libc", 158 | "windows-sys 0.52.0", 159 | ] 160 | 161 | [[package]] 162 | name = "fantoccini" 163 | version = "0.21.2" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "dd52b63e98251013cd5a9e881b9d460fc530e5df4eec58930c9694d6497c53e5" 166 | dependencies = [ 167 | "base64 0.22.1", 168 | "cookie 0.18.1", 169 | "futures-core", 170 | "futures-util", 171 | "http 1.1.0", 172 | "http-body-util", 173 | "hyper", 174 | "hyper-tls", 175 | "hyper-util", 176 | "mime", 177 | "openssl", 178 | "serde", 179 | "serde_json", 180 | "time", 181 | "tokio", 182 | "url", 183 | "webdriver", 184 | ] 185 | 186 | [[package]] 187 | name = "fastrand" 188 | version = "2.2.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" 191 | 192 | [[package]] 193 | name = "features" 194 | version = "0.1.0" 195 | dependencies = [ 196 | "json", 197 | "tinyweb", 198 | ] 199 | 200 | [[package]] 201 | name = "fnv" 202 | version = "1.0.7" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 205 | 206 | [[package]] 207 | name = "foreign-types" 208 | version = "0.3.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 211 | dependencies = [ 212 | "foreign-types-shared", 213 | ] 214 | 215 | [[package]] 216 | name = "foreign-types-shared" 217 | version = "0.1.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 220 | 221 | [[package]] 222 | name = "form_urlencoded" 223 | version = "1.2.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 226 | dependencies = [ 227 | "percent-encoding", 228 | ] 229 | 230 | [[package]] 231 | name = "futures-channel" 232 | version = "0.3.31" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 235 | dependencies = [ 236 | "futures-core", 237 | ] 238 | 239 | [[package]] 240 | name = "futures-core" 241 | version = "0.3.31" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 244 | 245 | [[package]] 246 | name = "futures-macro" 247 | version = "0.3.31" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 250 | dependencies = [ 251 | "proc-macro2", 252 | "quote", 253 | "syn", 254 | ] 255 | 256 | [[package]] 257 | name = "futures-task" 258 | version = "0.3.31" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 261 | 262 | [[package]] 263 | name = "futures-util" 264 | version = "0.3.31" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 267 | dependencies = [ 268 | "futures-core", 269 | "futures-macro", 270 | "futures-task", 271 | "pin-project-lite", 272 | "pin-utils", 273 | "slab", 274 | ] 275 | 276 | [[package]] 277 | name = "gimli" 278 | version = "0.31.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 281 | 282 | [[package]] 283 | name = "hermit-abi" 284 | version = "0.3.9" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 287 | 288 | [[package]] 289 | name = "http" 290 | version = "0.2.12" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 293 | dependencies = [ 294 | "bytes", 295 | "fnv", 296 | "itoa", 297 | ] 298 | 299 | [[package]] 300 | name = "http" 301 | version = "1.1.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 304 | dependencies = [ 305 | "bytes", 306 | "fnv", 307 | "itoa", 308 | ] 309 | 310 | [[package]] 311 | name = "http-body" 312 | version = "1.0.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 315 | dependencies = [ 316 | "bytes", 317 | "http 1.1.0", 318 | ] 319 | 320 | [[package]] 321 | name = "http-body-util" 322 | version = "0.1.2" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 325 | dependencies = [ 326 | "bytes", 327 | "futures-util", 328 | "http 1.1.0", 329 | "http-body", 330 | "pin-project-lite", 331 | ] 332 | 333 | [[package]] 334 | name = "httparse" 335 | version = "1.9.5" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 338 | 339 | [[package]] 340 | name = "hyper" 341 | version = "1.5.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" 344 | dependencies = [ 345 | "bytes", 346 | "futures-channel", 347 | "futures-util", 348 | "http 1.1.0", 349 | "http-body", 350 | "httparse", 351 | "itoa", 352 | "pin-project-lite", 353 | "smallvec", 354 | "tokio", 355 | "want", 356 | ] 357 | 358 | [[package]] 359 | name = "hyper-tls" 360 | version = "0.6.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 363 | dependencies = [ 364 | "bytes", 365 | "http-body-util", 366 | "hyper", 367 | "hyper-util", 368 | "native-tls", 369 | "tokio", 370 | "tokio-native-tls", 371 | "tower-service", 372 | ] 373 | 374 | [[package]] 375 | name = "hyper-util" 376 | version = "0.1.10" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 379 | dependencies = [ 380 | "bytes", 381 | "futures-channel", 382 | "futures-util", 383 | "http 1.1.0", 384 | "http-body", 385 | "hyper", 386 | "pin-project-lite", 387 | "socket2", 388 | "tokio", 389 | "tower-service", 390 | "tracing", 391 | ] 392 | 393 | [[package]] 394 | name = "icu_collections" 395 | version = "1.5.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 398 | dependencies = [ 399 | "displaydoc", 400 | "yoke", 401 | "zerofrom", 402 | "zerovec", 403 | ] 404 | 405 | [[package]] 406 | name = "icu_locid" 407 | version = "1.5.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 410 | dependencies = [ 411 | "displaydoc", 412 | "litemap", 413 | "tinystr", 414 | "writeable", 415 | "zerovec", 416 | ] 417 | 418 | [[package]] 419 | name = "icu_locid_transform" 420 | version = "1.5.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 423 | dependencies = [ 424 | "displaydoc", 425 | "icu_locid", 426 | "icu_locid_transform_data", 427 | "icu_provider", 428 | "tinystr", 429 | "zerovec", 430 | ] 431 | 432 | [[package]] 433 | name = "icu_locid_transform_data" 434 | version = "1.5.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 437 | 438 | [[package]] 439 | name = "icu_normalizer" 440 | version = "1.5.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 443 | dependencies = [ 444 | "displaydoc", 445 | "icu_collections", 446 | "icu_normalizer_data", 447 | "icu_properties", 448 | "icu_provider", 449 | "smallvec", 450 | "utf16_iter", 451 | "utf8_iter", 452 | "write16", 453 | "zerovec", 454 | ] 455 | 456 | [[package]] 457 | name = "icu_normalizer_data" 458 | version = "1.5.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 461 | 462 | [[package]] 463 | name = "icu_properties" 464 | version = "1.5.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 467 | dependencies = [ 468 | "displaydoc", 469 | "icu_collections", 470 | "icu_locid_transform", 471 | "icu_properties_data", 472 | "icu_provider", 473 | "tinystr", 474 | "zerovec", 475 | ] 476 | 477 | [[package]] 478 | name = "icu_properties_data" 479 | version = "1.5.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 482 | 483 | [[package]] 484 | name = "icu_provider" 485 | version = "1.5.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 488 | dependencies = [ 489 | "displaydoc", 490 | "icu_locid", 491 | "icu_provider_macros", 492 | "stable_deref_trait", 493 | "tinystr", 494 | "writeable", 495 | "yoke", 496 | "zerofrom", 497 | "zerovec", 498 | ] 499 | 500 | [[package]] 501 | name = "icu_provider_macros" 502 | version = "1.5.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 505 | dependencies = [ 506 | "proc-macro2", 507 | "quote", 508 | "syn", 509 | ] 510 | 511 | [[package]] 512 | name = "idna" 513 | version = "1.0.3" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 516 | dependencies = [ 517 | "idna_adapter", 518 | "smallvec", 519 | "utf8_iter", 520 | ] 521 | 522 | [[package]] 523 | name = "idna_adapter" 524 | version = "1.2.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 527 | dependencies = [ 528 | "icu_normalizer", 529 | "icu_properties", 530 | ] 531 | 532 | [[package]] 533 | name = "itoa" 534 | version = "1.0.11" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 537 | 538 | [[package]] 539 | name = "json" 540 | version = "0.12.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" 543 | 544 | [[package]] 545 | name = "libc" 546 | version = "0.2.162" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" 549 | 550 | [[package]] 551 | name = "linux-raw-sys" 552 | version = "0.4.14" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 555 | 556 | [[package]] 557 | name = "litemap" 558 | version = "0.7.3" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" 561 | 562 | [[package]] 563 | name = "lock_api" 564 | version = "0.4.12" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 567 | dependencies = [ 568 | "autocfg", 569 | "scopeguard", 570 | ] 571 | 572 | [[package]] 573 | name = "log" 574 | version = "0.4.22" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 577 | 578 | [[package]] 579 | name = "memchr" 580 | version = "2.7.4" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 583 | 584 | [[package]] 585 | name = "mime" 586 | version = "0.3.17" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 589 | 590 | [[package]] 591 | name = "minimal" 592 | version = "0.1.0" 593 | dependencies = [ 594 | "tinyweb", 595 | ] 596 | 597 | [[package]] 598 | name = "miniz_oxide" 599 | version = "0.8.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 602 | dependencies = [ 603 | "adler2", 604 | ] 605 | 606 | [[package]] 607 | name = "mio" 608 | version = "1.0.2" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 611 | dependencies = [ 612 | "hermit-abi", 613 | "libc", 614 | "wasi", 615 | "windows-sys 0.52.0", 616 | ] 617 | 618 | [[package]] 619 | name = "native-tls" 620 | version = "0.2.12" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 623 | dependencies = [ 624 | "libc", 625 | "log", 626 | "openssl", 627 | "openssl-probe", 628 | "openssl-sys", 629 | "schannel", 630 | "security-framework", 631 | "security-framework-sys", 632 | "tempfile", 633 | ] 634 | 635 | [[package]] 636 | name = "num-conv" 637 | version = "0.1.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 640 | 641 | [[package]] 642 | name = "object" 643 | version = "0.36.5" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 646 | dependencies = [ 647 | "memchr", 648 | ] 649 | 650 | [[package]] 651 | name = "once_cell" 652 | version = "1.20.2" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 655 | 656 | [[package]] 657 | name = "openssl" 658 | version = "0.10.68" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" 661 | dependencies = [ 662 | "bitflags", 663 | "cfg-if", 664 | "foreign-types", 665 | "libc", 666 | "once_cell", 667 | "openssl-macros", 668 | "openssl-sys", 669 | ] 670 | 671 | [[package]] 672 | name = "openssl-macros" 673 | version = "0.1.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 676 | dependencies = [ 677 | "proc-macro2", 678 | "quote", 679 | "syn", 680 | ] 681 | 682 | [[package]] 683 | name = "openssl-probe" 684 | version = "0.1.5" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 687 | 688 | [[package]] 689 | name = "openssl-sys" 690 | version = "0.9.104" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" 693 | dependencies = [ 694 | "cc", 695 | "libc", 696 | "pkg-config", 697 | "vcpkg", 698 | ] 699 | 700 | [[package]] 701 | name = "parking_lot" 702 | version = "0.12.3" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 705 | dependencies = [ 706 | "lock_api", 707 | "parking_lot_core", 708 | ] 709 | 710 | [[package]] 711 | name = "parking_lot_core" 712 | version = "0.9.10" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 715 | dependencies = [ 716 | "cfg-if", 717 | "libc", 718 | "redox_syscall", 719 | "smallvec", 720 | "windows-targets", 721 | ] 722 | 723 | [[package]] 724 | name = "percent-encoding" 725 | version = "2.3.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 728 | 729 | [[package]] 730 | name = "pin-project-lite" 731 | version = "0.2.15" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 734 | 735 | [[package]] 736 | name = "pin-utils" 737 | version = "0.1.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 740 | 741 | [[package]] 742 | name = "pkg-config" 743 | version = "0.3.31" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 746 | 747 | [[package]] 748 | name = "powerfmt" 749 | version = "0.2.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 752 | 753 | [[package]] 754 | name = "proc-macro2" 755 | version = "1.0.89" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 758 | dependencies = [ 759 | "unicode-ident", 760 | ] 761 | 762 | [[package]] 763 | name = "quote" 764 | version = "1.0.37" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 767 | dependencies = [ 768 | "proc-macro2", 769 | ] 770 | 771 | [[package]] 772 | name = "redox_syscall" 773 | version = "0.5.7" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 776 | dependencies = [ 777 | "bitflags", 778 | ] 779 | 780 | [[package]] 781 | name = "rustc-demangle" 782 | version = "0.1.24" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 785 | 786 | [[package]] 787 | name = "rustix" 788 | version = "0.38.39" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" 791 | dependencies = [ 792 | "bitflags", 793 | "errno", 794 | "libc", 795 | "linux-raw-sys", 796 | "windows-sys 0.52.0", 797 | ] 798 | 799 | [[package]] 800 | name = "ryu" 801 | version = "1.0.18" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 804 | 805 | [[package]] 806 | name = "schannel" 807 | version = "0.1.26" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" 810 | dependencies = [ 811 | "windows-sys 0.59.0", 812 | ] 813 | 814 | [[package]] 815 | name = "scopeguard" 816 | version = "1.2.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 819 | 820 | [[package]] 821 | name = "security-framework" 822 | version = "2.11.1" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 825 | dependencies = [ 826 | "bitflags", 827 | "core-foundation", 828 | "core-foundation-sys", 829 | "libc", 830 | "security-framework-sys", 831 | ] 832 | 833 | [[package]] 834 | name = "security-framework-sys" 835 | version = "2.12.1" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" 838 | dependencies = [ 839 | "core-foundation-sys", 840 | "libc", 841 | ] 842 | 843 | [[package]] 844 | name = "serde" 845 | version = "1.0.214" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 848 | dependencies = [ 849 | "serde_derive", 850 | ] 851 | 852 | [[package]] 853 | name = "serde_derive" 854 | version = "1.0.214" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 857 | dependencies = [ 858 | "proc-macro2", 859 | "quote", 860 | "syn", 861 | ] 862 | 863 | [[package]] 864 | name = "serde_json" 865 | version = "1.0.132" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 868 | dependencies = [ 869 | "itoa", 870 | "memchr", 871 | "ryu", 872 | "serde", 873 | ] 874 | 875 | [[package]] 876 | name = "shlex" 877 | version = "1.3.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 880 | 881 | [[package]] 882 | name = "signal-hook-registry" 883 | version = "1.4.2" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 886 | dependencies = [ 887 | "libc", 888 | ] 889 | 890 | [[package]] 891 | name = "slab" 892 | version = "0.4.9" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 895 | dependencies = [ 896 | "autocfg", 897 | ] 898 | 899 | [[package]] 900 | name = "smallvec" 901 | version = "1.13.2" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 904 | 905 | [[package]] 906 | name = "socket2" 907 | version = "0.5.7" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 910 | dependencies = [ 911 | "libc", 912 | "windows-sys 0.52.0", 913 | ] 914 | 915 | [[package]] 916 | name = "stable_deref_trait" 917 | version = "1.2.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 920 | 921 | [[package]] 922 | name = "syn" 923 | version = "2.0.87" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 926 | dependencies = [ 927 | "proc-macro2", 928 | "quote", 929 | "unicode-ident", 930 | ] 931 | 932 | [[package]] 933 | name = "synstructure" 934 | version = "0.13.1" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 937 | dependencies = [ 938 | "proc-macro2", 939 | "quote", 940 | "syn", 941 | ] 942 | 943 | [[package]] 944 | name = "tempfile" 945 | version = "3.14.0" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 948 | dependencies = [ 949 | "cfg-if", 950 | "fastrand", 951 | "once_cell", 952 | "rustix", 953 | "windows-sys 0.59.0", 954 | ] 955 | 956 | [[package]] 957 | name = "thiserror" 958 | version = "1.0.69" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 961 | dependencies = [ 962 | "thiserror-impl", 963 | ] 964 | 965 | [[package]] 966 | name = "thiserror-impl" 967 | version = "1.0.69" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 970 | dependencies = [ 971 | "proc-macro2", 972 | "quote", 973 | "syn", 974 | ] 975 | 976 | [[package]] 977 | name = "time" 978 | version = "0.3.36" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 981 | dependencies = [ 982 | "deranged", 983 | "itoa", 984 | "num-conv", 985 | "powerfmt", 986 | "serde", 987 | "time-core", 988 | "time-macros", 989 | ] 990 | 991 | [[package]] 992 | name = "time-core" 993 | version = "0.1.2" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 996 | 997 | [[package]] 998 | name = "time-macros" 999 | version = "0.2.18" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1002 | dependencies = [ 1003 | "num-conv", 1004 | "time-core", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "tinystr" 1009 | version = "0.7.6" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1012 | dependencies = [ 1013 | "displaydoc", 1014 | "zerovec", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "tinyweb" 1019 | version = "0.1.0" 1020 | dependencies = [ 1021 | "fantoccini", 1022 | "serde_json", 1023 | "tokio", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "tokio" 1028 | version = "1.41.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" 1031 | dependencies = [ 1032 | "backtrace", 1033 | "bytes", 1034 | "libc", 1035 | "mio", 1036 | "parking_lot", 1037 | "pin-project-lite", 1038 | "signal-hook-registry", 1039 | "socket2", 1040 | "tokio-macros", 1041 | "windows-sys 0.52.0", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "tokio-macros" 1046 | version = "2.4.0" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1049 | dependencies = [ 1050 | "proc-macro2", 1051 | "quote", 1052 | "syn", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "tokio-native-tls" 1057 | version = "0.3.1" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1060 | dependencies = [ 1061 | "native-tls", 1062 | "tokio", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "tower-service" 1067 | version = "0.3.3" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1070 | 1071 | [[package]] 1072 | name = "tracing" 1073 | version = "0.1.40" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1076 | dependencies = [ 1077 | "pin-project-lite", 1078 | "tracing-core", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "tracing-core" 1083 | version = "0.1.32" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1086 | dependencies = [ 1087 | "once_cell", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "try-lock" 1092 | version = "0.2.5" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1095 | 1096 | [[package]] 1097 | name = "unicode-ident" 1098 | version = "1.0.13" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1101 | 1102 | [[package]] 1103 | name = "unicode-segmentation" 1104 | version = "1.12.0" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1107 | 1108 | [[package]] 1109 | name = "url" 1110 | version = "2.5.3" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" 1113 | dependencies = [ 1114 | "form_urlencoded", 1115 | "idna", 1116 | "percent-encoding", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "utf16_iter" 1121 | version = "1.0.5" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1124 | 1125 | [[package]] 1126 | name = "utf8_iter" 1127 | version = "1.0.4" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1130 | 1131 | [[package]] 1132 | name = "vcpkg" 1133 | version = "0.2.15" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1136 | 1137 | [[package]] 1138 | name = "version_check" 1139 | version = "0.9.5" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1142 | 1143 | [[package]] 1144 | name = "want" 1145 | version = "0.3.1" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1148 | dependencies = [ 1149 | "try-lock", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "wasi" 1154 | version = "0.11.0+wasi-snapshot-preview1" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1157 | 1158 | [[package]] 1159 | name = "webdriver" 1160 | version = "0.50.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71" 1163 | dependencies = [ 1164 | "base64 0.21.7", 1165 | "bytes", 1166 | "cookie 0.16.2", 1167 | "http 0.2.12", 1168 | "log", 1169 | "serde", 1170 | "serde_derive", 1171 | "serde_json", 1172 | "thiserror", 1173 | "time", 1174 | "unicode-segmentation", 1175 | "url", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "windows-sys" 1180 | version = "0.52.0" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1183 | dependencies = [ 1184 | "windows-targets", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "windows-sys" 1189 | version = "0.59.0" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1192 | dependencies = [ 1193 | "windows-targets", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "windows-targets" 1198 | version = "0.52.6" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1201 | dependencies = [ 1202 | "windows_aarch64_gnullvm", 1203 | "windows_aarch64_msvc", 1204 | "windows_i686_gnu", 1205 | "windows_i686_gnullvm", 1206 | "windows_i686_msvc", 1207 | "windows_x86_64_gnu", 1208 | "windows_x86_64_gnullvm", 1209 | "windows_x86_64_msvc", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "windows_aarch64_gnullvm" 1214 | version = "0.52.6" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1217 | 1218 | [[package]] 1219 | name = "windows_aarch64_msvc" 1220 | version = "0.52.6" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1223 | 1224 | [[package]] 1225 | name = "windows_i686_gnu" 1226 | version = "0.52.6" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1229 | 1230 | [[package]] 1231 | name = "windows_i686_gnullvm" 1232 | version = "0.52.6" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1235 | 1236 | [[package]] 1237 | name = "windows_i686_msvc" 1238 | version = "0.52.6" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1241 | 1242 | [[package]] 1243 | name = "windows_x86_64_gnu" 1244 | version = "0.52.6" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1247 | 1248 | [[package]] 1249 | name = "windows_x86_64_gnullvm" 1250 | version = "0.52.6" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1253 | 1254 | [[package]] 1255 | name = "windows_x86_64_msvc" 1256 | version = "0.52.6" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1259 | 1260 | [[package]] 1261 | name = "write16" 1262 | version = "1.0.0" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1265 | 1266 | [[package]] 1267 | name = "writeable" 1268 | version = "0.5.5" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1271 | 1272 | [[package]] 1273 | name = "yoke" 1274 | version = "0.7.4" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" 1277 | dependencies = [ 1278 | "serde", 1279 | "stable_deref_trait", 1280 | "yoke-derive", 1281 | "zerofrom", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "yoke-derive" 1286 | version = "0.7.4" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" 1289 | dependencies = [ 1290 | "proc-macro2", 1291 | "quote", 1292 | "syn", 1293 | "synstructure", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "zerofrom" 1298 | version = "0.1.4" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" 1301 | dependencies = [ 1302 | "zerofrom-derive", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "zerofrom-derive" 1307 | version = "0.1.4" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" 1310 | dependencies = [ 1311 | "proc-macro2", 1312 | "quote", 1313 | "syn", 1314 | "synstructure", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "zerovec" 1319 | version = "0.10.4" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1322 | dependencies = [ 1323 | "yoke", 1324 | "zerofrom", 1325 | "zerovec-derive", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "zerovec-derive" 1330 | version = "0.10.3" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1333 | dependencies = [ 1334 | "proc-macro2", 1335 | "quote", 1336 | "syn", 1337 | ] 1338 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "src/rust", 5 | "examples/minimal", 6 | "examples/features", 7 | "examples/dom", 8 | "examples/callbacks", 9 | ] 10 | default-members = ["src/rust"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andreas Tzionis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinyWeb 🌱 Rust on the client. No dependencies. 2 | 3 | Build the client side with Rust! Backend agnostic. Less than 800 lines of code. 4 | 5 | # What's TinyWeb? 6 | 7 | TinyWeb is a toolkit for building web applications focused on both correctness and simplicity. 8 | 9 | Enables client-side applications to be built in pure Rust, similar to backend applications, leveraging the language strict type system and great built-in tooling. Has a tiny footprint with less than 800 lines of code, has no build step and no external dependencies. 10 | 11 | 12 | # Features 13 | 14 | - No Javascript 15 | - No macros 16 | - No dependencies 17 | - No build step 18 | - Just HTML & Rust (Wasm) 19 | 20 | **Note:** No build step besides `cargo build` 21 | 22 | # Getting Started 23 | 24 | ### Use the starter project 25 | 26 | - Fork the [tinyweb-starter](https://github.com/LiveDuo/tinyweb-starter) project 27 | 28 | [![Tutorial](https://raw.githubusercontent.com/LiveDuo/tinyweb/master/.github/assets/tinyweb-youtube.jpg)](https://www.youtube.com/watch?v=44P3IVnjEqo "Tutorial") 29 | 30 | ### Create a new project 31 | 32 | 1. Create a new Rust project with `cargo new tinyweb-example --lib`. Add `crate-type =["cdylib"]` in `Cargo.toml` and install the crate with `cargo add tinyweb --git https://github.com/LiveDuo/tinyweb`. 33 | 34 | 2. Update the `src/lib.rs`: 35 | ```rs 36 | use tinyweb::element::El; 37 | use tinyweb::invoke::Js; 38 | 39 | fn component() -> El { 40 | El::new("div") 41 | .child(El::new("button").text("print").on("click", move |_| { 42 | Js::invoke("alert('hello browser')", &[]); 43 | })) 44 | } 45 | 46 | #[no_mangle] 47 | pub fn main() { 48 | let body = Js::invoke("return document.querySelector('body')", &[]).to_ref().unwrap(); 49 | component().mount(&body); 50 | } 51 | ``` 52 | 53 | 3. Create an `index.html` in a new `public` folder: 54 | ```html 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ``` 65 | 66 | 67 | 68 | 4. Build the project with `cargo build --target wasm32-unknown-unknown -r`. Then `cp target/wasm32-unknown-unknown/release/*.wasm public/client.wasm` to get the `.wasm` in the right place and serve the `public` folder with any static http server. 69 | 70 | 71 | 72 | # How it works 73 | 74 | **Initialization:** Each project built with TinyWeb has 3 components, an `index.html`, a static `main.js` and a `client.wasm` file compiled from Rust with `cargo build --target wasm32-unknown-unknown -r`. These files can be served with any static HTTP server. When the website is visited, the `index.html` file loads the [main.js](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/js/main.js) file which registers a [DOMContentLoaded](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/js/main.js) event listener. When the page finishes loading, the listener is triggered which [calls](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/js/main.js) the `main` function in the wasm file (usually making the initial DOM rendering and registering event listeners). 75 | 76 | **Browser APIs:** When a Rust function wants to invoke a browser API, it uses the [__invoke](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/rust/src/invoke.rs) function internally, which in turn calls its [counterpart](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/js/main.js) in Javascript. 77 | 78 | **Callbacks:** When a listener is registered in Rust, it takes a callback function as a parameter and that function is stored in [CALLBACK_HANDLERS](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/rust/src/callbacks.rs). Every time the callback is triggered, the [handle_callback](https://github.com/LiveDuo/tinyweb/blob/feature/readme/src/rust/src/handlers.rs) function is called which executes the callback function that was stored earlier. 79 | 80 | # How to's & guides 81 | 82 | ### Browser APIs 83 | 84 | ```rs 85 | use tinyweb::invoke::Js; 86 | 87 | Js::invoke("alert('hello browser')", &[]); 88 | ``` 89 | 90 | Check it out [here](https://github.com/LiveDuo/tinyweb/blob/feature/readme/examples/features/src/lib.rs) 91 | 92 | ### Reactivity and Signals 93 | 94 | ```rs 95 | 96 | use tinyweb::signals::Signal; 97 | use tinyweb::element::El; 98 | 99 | let signal_count = Signal::new(0); 100 | 101 | El::new("button").text("add").on("click", move |_| { 102 | let count = signal_count.get() + 1; 103 | signal_count.set(count); 104 | }); 105 | ``` 106 | 107 | Check it out [here](https://github.com/LiveDuo/tinyweb/blob/feature/readme/examples/features/src/lib.rs) 108 | 109 | ### Router support 110 | 111 | ```rs 112 | use tinyweb::router::{Page, Router}; 113 | 114 | thread_local! { 115 | pub static ROUTER: RefCell = RefCell::new(Router::default()); 116 | } 117 | 118 | // initialize router 119 | let pages = &[Page::new("/page1", page_component())]; 120 | ROUTER.with(|s| { *s.borrow_mut() = Router::new("body", pages); }); 121 | 122 | // navigate to route 123 | ROUTER.with(|s| { s.borrow().navigate("/page1"); }); 124 | ``` 125 | 126 | Check it out [here](https://github.com/LiveDuo/tinyweb/blob/feature/readme/examples/features/src/lib.rs) 127 | 128 | ### Async Support 129 | 130 | ```rs 131 | use tinyweb::runtime::Runtime; 132 | use tinyweb::invoke::Js; 133 | 134 | Runtime::block_on(async move { 135 | Runtime::promise("window.setTimeout({},{})", move |c| vec![c.into(), 1_000.into()]).await; 136 | Js::invoke("alert('timer')"); 137 | }); 138 | ``` 139 | 140 | Check it out [here](https://github.com/LiveDuo/tinyweb/blob/feature/readme/examples/features/src/lib.rs) 141 | 142 | # Roadmap 143 | 144 | ### Components & Utilities 145 | 146 | While this library tries to be minimal and has no dependencies the reality in web development is using libraries and ready-made components especially for a few slightly annoying tasks. Here are some ideas for commonly used utilities and UI components. Utilities can be included in the `examples` folder while components can be stored in a new `components` folder in this repo. 147 | 148 |
149 | Commonly used utilities 150 | 151 |
152 | 153 | - [ ] Drag & drop / resize 154 | - [ ] File upload 155 | - [ ] Markdown rendering 156 | 157 |
158 | 159 |
160 | Commonly used components 161 | 162 |
163 | 164 | - [ ] Table components 165 | - [ ] Modals, tooltips and toasts 166 | - [ ] Date / time pickers 167 | - [ ] Chart / visualization 168 | 169 |
170 | 171 | ### Benchmarks & Profiling 172 | 173 | Need benchmarks to see how this library performs against other Rust web frameworks but also against different Javascript frameworks. Need also profiling to evaluate if there are memory leaks in either Rust side or Javascript side of the library and to figure out if the compiled WASM size can be reduced further. 174 | 175 | ### Static analysis 176 | 177 | Right now `invoke` calls to the browser APIs are not type safe. Could use [webidl](https://github.com/whatwg/webidl) interfaces to do static analysis on the Javascript code against `invoke` parameters. 178 | 179 | 180 | # Backstory 181 | 182 |
183 | Show 184 | 185 | For quite some time, I couldn't decide if I like Typescript or not. On one hand, it offers stronger typing than pure JavaScript, providing more confidence in the code; but on the other hand, it comes with a heavy build system that complicates things and makes debugging significantly harder. 186 | 187 | When I had to build an application where I really cared about correctness, I realized how much I didn't trust Typescript even for what's designed to do and I tried different Rust based web frameworks instead. While these frameworks alleviated correctness concerns, they introduced significant complexity, requiring hundreds of dependencies just to get started. For reference, `leptos` depends on 231 crates and its development tool `cargo-leptos` depends on another 485 crates. 188 | 189 | Many of these dependencies come from the `wasm-bindgen` crate, which generates Rust bindings for browser APIs and the JavaScript glue code needed for these calls and is used almost universally by Rust based web frameworks as a lower level building block for accessing browser APIs. 190 | 191 | Yet, using this crate is not the only way to interact with browser APIs and many applications could benefit from a different tool that makes different tradeoffs. In particular, many applications might benefit from simplicity and ease of debugging, I know the application I'm building probably would. 192 | 193 | So, I set out to build a web framework that allows to build client side applications with Rust and has minimal footprint. The result is `TinyWeb`, a client side Rust framework built in <800 lines of code. 194 | 195 |
196 | 197 | # Credits 198 | 199 | Credits to [Richard Anaya](https://github.com/richardanaya) for his work on [web.rs](https://github.com/richardanaya/web.rs) that provided ideas to practical challenges on [async support](https://github.com/richardanaya/web.rs/blob/master/crates/web/src/executor.rs). Also, to [Greg Johnston](https://github.com/gbj) for [his videos](https://www.youtube.com/@gbjxc/videos) that show how to use Solid.js-like signals in Rust. 200 | -------------------------------------------------------------------------------- /examples/callbacks/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/callbacks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "callbacks" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | test = false 9 | 10 | [dependencies] 11 | tinyweb = { path = "../../src/rust" } 12 | -------------------------------------------------------------------------------- /examples/callbacks/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --target wasm32-unknown-unknown -r 3 | mkdir -p /tmp/public 4 | cp ../../target/wasm32-unknown-unknown/release/callbacks.wasm /tmp/public/callbacks.wasm 5 | cp ../../src/js/main.js /tmp/public/main.js 6 | cp public/index.html /tmp/public/index.html 7 | start: 8 | python3 -m http.server -d /tmp/public 9 | dev: 10 | make build 11 | make start 12 | -------------------------------------------------------------------------------- /examples/callbacks/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/callbacks/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | use tinyweb::callbacks::{create_async_callback, create_callback}; 3 | use tinyweb::runtime::Runtime; 4 | use tinyweb::invoke::*; 5 | 6 | #[no_mangle] 7 | pub fn main() { 8 | 9 | std::panic::set_hook(Box::new(|e| { Js::invoke("console.log({})", &[e.to_string().into()]); })); 10 | 11 | // invoke 12 | Js::invoke("console.log('invoke')", &[]); 13 | 14 | // invoke callback 15 | let function_ref = create_callback(move |_s| { Js::invoke("console.log('invoke timer')", &[]); }); 16 | Js::invoke("setTimeout({}, 1000)", &[function_ref.into()]); 17 | 18 | // invoke async callback 19 | let url = "https://pokeapi.co/api/v2/pokemon/1"; 20 | let (callback_ref, future) = create_async_callback(); 21 | Js::invoke("fetch({}).then(r => r.json()).then(r => { {}(r) })", &[url.into(), callback_ref.into()]); 22 | Runtime::block_on(async move { 23 | let future_leak = Box::leak(Box::new(future)); 24 | let object_ref = future_leak.await; 25 | let result = Js::invoke("return {}.name", &[object_ref.into()]).to_str().unwrap(); 26 | Js::invoke("console.log('invoke fetch', {})", &[result.into()]); 27 | Js::deallocate(object_ref); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /examples/dom/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dom" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | test = false 9 | 10 | [dependencies] 11 | tinyweb = { path = "../../src/rust" } 12 | -------------------------------------------------------------------------------- /examples/dom/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --target wasm32-unknown-unknown -r 3 | mkdir -p /tmp/public 4 | cp ../../target/wasm32-unknown-unknown/release/dom.wasm /tmp/public/dom.wasm 5 | cp ../../src/js/main.js /tmp/public/main.js 6 | cp public/index.html /tmp/public/index.html 7 | start: 8 | python3 -m http.server -d /tmp/public 9 | dev: 10 | make build 11 | make start 12 | -------------------------------------------------------------------------------- /examples/dom/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/dom/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | use tinyweb::callbacks::create_callback; 3 | use tinyweb::invoke::*; 4 | 5 | #[no_mangle] 6 | pub fn main() { 7 | 8 | std::panic::set_hook(Box::new(|e| { Js::invoke("console.log({})", &[e.to_string().into()]); })); 9 | 10 | let button = Js::invoke("return document.createElement('button')", &[]).to_ref().unwrap(); 11 | Js::invoke("{}.textContent = 'Click'", &[button.into()]); 12 | 13 | let function_ref = create_callback(move |e| { Js::invoke("alert('hello')", &[]); Js::deallocate(e); }); 14 | Js::invoke("{}.addEventListener('click',{})", &[button.into(), function_ref.into()]); 15 | 16 | let body = Js::invoke("return document.querySelector('body')", &[]).to_ref().unwrap(); 17 | Js::invoke("{}.appendChild({})", &[body.into(), button.into()]); 18 | } 19 | -------------------------------------------------------------------------------- /examples/features/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/features/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "features" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type =["cdylib"] 8 | test = false 9 | 10 | [dependencies] 11 | tinyweb = { path = "../../src/rust" } 12 | json = "0.12.4" 13 | 14 | -------------------------------------------------------------------------------- /examples/features/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --target wasm32-unknown-unknown -r 3 | mkdir -p /tmp/public 4 | cp ../../target/wasm32-unknown-unknown/release/features.wasm /tmp/public/features.wasm 5 | cp ../../src/js/main.js /tmp/public/main.js 6 | cp public/index.html /tmp/public/index.html 7 | start: 8 | python3 -m http.server -d /tmp/public 9 | dev: 10 | make build 11 | make start 12 | -------------------------------------------------------------------------------- /examples/features/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/features/src/keycodes.rs: -------------------------------------------------------------------------------- 1 | 2 | // https://stackoverflow.com/a/23377822/413726 3 | // names of known key codes (0-255) 4 | 5 | pub const KEYBOARD_MAP: &[&str] = &[ 6 | "", // [0] 7 | "", // [1] 8 | "", // [2] 9 | "CANCEL", // [3] 10 | "", // [4] 11 | "", // [5] 12 | "HELP", // [6] 13 | "", // [7] 14 | "BACK_SPACE", // [8] 15 | "TAB", // [9] 16 | "", // [10] 17 | "", // [11] 18 | "CLEAR", // [12] 19 | "ENTER", // [13] 20 | "ENTER_SPECIAL", // [14] 21 | "", // [15] 22 | "SHIFT", // [16] 23 | "CONTROL", // [17] 24 | "ALT", // [18] 25 | "PAUSE", // [19] 26 | "CAPS_LOCK", // [20] 27 | "KANA", // [21] 28 | "EISU", // [22] 29 | "JUNJA", // [23] 30 | "FINAL", // [24] 31 | "HANJA", // [25] 32 | "", // [26] 33 | "ESCAPE", // [27] 34 | "CONVERT", // [28] 35 | "NONCONVERT", // [29] 36 | "ACCEPT", // [30] 37 | "MODECHANGE", // [31] 38 | "SPACE", // [32] 39 | "PAGE_UP", // [33] 40 | "PAGE_DOWN", // [34] 41 | "END", // [35] 42 | "HOME", // [36] 43 | "LEFT", // [37] 44 | "UP", // [38] 45 | "RIGHT", // [39] 46 | "DOWN", // [40] 47 | "SELECT", // [41] 48 | "PRINT", // [42] 49 | "EXECUTE", // [43] 50 | "PRINTSCREEN", // [44] 51 | "INSERT", // [45] 52 | "DELETE", // [46] 53 | "", // [47] 54 | "0", // [48] 55 | "1", // [49] 56 | "2", // [50] 57 | "3", // [51] 58 | "4", // [52] 59 | "5", // [53] 60 | "6", // [54] 61 | "7", // [55] 62 | "8", // [56] 63 | "9", // [57] 64 | "COLON", // [58] 65 | "SEMICOLON", // [59] 66 | "LESS_THAN", // [60] 67 | "EQUALS", // [61] 68 | "GREATER_THAN", // [62] 69 | "QUESTION_MARK", // [63] 70 | "AT", // [64] 71 | "A", // [65] 72 | "B", // [66] 73 | "C", // [67] 74 | "D", // [68] 75 | "E", // [69] 76 | "F", // [70] 77 | "G", // [71] 78 | "H", // [72] 79 | "I", // [73] 80 | "J", // [74] 81 | "K", // [75] 82 | "L", // [76] 83 | "M", // [77] 84 | "N", // [78] 85 | "O", // [79] 86 | "P", // [80] 87 | "Q", // [81] 88 | "R", // [82] 89 | "S", // [83] 90 | "T", // [84] 91 | "U", // [85] 92 | "V", // [86] 93 | "W", // [87] 94 | "X", // [88] 95 | "Y", // [89] 96 | "Z", // [90] 97 | "OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac) 98 | "", // [92] 99 | "CONTEXT_MENU", // [93] 100 | "", // [94] 101 | "SLEEP", // [95] 102 | "NUMPAD0", // [96] 103 | "NUMPAD1", // [97] 104 | "NUMPAD2", // [98] 105 | "NUMPAD3", // [99] 106 | "NUMPAD4", // [100] 107 | "NUMPAD5", // [101] 108 | "NUMPAD6", // [102] 109 | "NUMPAD7", // [103] 110 | "NUMPAD8", // [104] 111 | "NUMPAD9", // [105] 112 | "MULTIPLY", // [106] 113 | "ADD", // [107] 114 | "SEPARATOR", // [108] 115 | "SUBTRACT", // [109] 116 | "DECIMAL", // [110] 117 | "DIVIDE", // [111] 118 | "F1", // [112] 119 | "F2", // [113] 120 | "F3", // [114] 121 | "F4", // [115] 122 | "F5", // [116] 123 | "F6", // [117] 124 | "F7", // [118] 125 | "F8", // [119] 126 | "F9", // [120] 127 | "F10", // [121] 128 | "F11", // [122] 129 | "F12", // [123] 130 | "F13", // [124] 131 | "F14", // [125] 132 | "F15", // [126] 133 | "F16", // [127] 134 | "F17", // [128] 135 | "F18", // [129] 136 | "F19", // [130] 137 | "F20", // [131] 138 | "F21", // [132] 139 | "F22", // [133] 140 | "F23", // [134] 141 | "F24", // [135] 142 | "", // [136] 143 | "", // [137] 144 | "", // [138] 145 | "", // [139] 146 | "", // [140] 147 | "", // [141] 148 | "", // [142] 149 | "", // [143] 150 | "NUM_LOCK", // [144] 151 | "SCROLL_LOCK", // [145] 152 | "WIN_OEM_FJ_JISHO", // [146] 153 | "WIN_OEM_FJ_MASSHOU", // [147] 154 | "WIN_OEM_FJ_TOUROKU", // [148] 155 | "WIN_OEM_FJ_LOYA", // [149] 156 | "WIN_OEM_FJ_ROYA", // [150] 157 | "", // [151] 158 | "", // [152] 159 | "", // [153] 160 | "", // [154] 161 | "", // [155] 162 | "", // [156] 163 | "", // [157] 164 | "", // [158] 165 | "", // [159] 166 | "CIRCUMFLEX", // [160] 167 | "EXCLAMATION", // [161] 168 | "DOUBLE_QUOTE", // [162] 169 | "HASH", // [163] 170 | "DOLLAR", // [164] 171 | "PERCENT", // [165] 172 | "AMPERSAND", // [166] 173 | "UNDERSCORE", // [167] 174 | "OPEN_PAREN", // [168] 175 | "CLOSE_PAREN", // [169] 176 | "ASTERISK", // [170] 177 | "PLUS", // [171] 178 | "PIPE", // [172] 179 | "HYPHEN_MINUS", // [173] 180 | "OPEN_CURLY_BRACKET", // [174] 181 | "CLOSE_CURLY_BRACKET", // [175] 182 | "TILDE", // [176] 183 | "", // [177] 184 | "", // [178] 185 | "", // [179] 186 | "", // [180] 187 | "VOLUME_MUTE", // [181] 188 | "VOLUME_DOWN", // [182] 189 | "VOLUME_UP", // [183] 190 | "", // [184] 191 | "", // [185] 192 | "SEMICOLON", // [186] 193 | "EQUALS", // [187] 194 | "COMMA", // [188] 195 | "MINUS", // [189] 196 | "PERIOD", // [190] 197 | "SLASH", // [191] 198 | "BACK_QUOTE", // [192] 199 | "", // [193] 200 | "", // [194] 201 | "", // [195] 202 | "", // [196] 203 | "", // [197] 204 | "", // [198] 205 | "", // [199] 206 | "", // [200] 207 | "", // [201] 208 | "", // [202] 209 | "", // [203] 210 | "", // [204] 211 | "", // [205] 212 | "", // [206] 213 | "", // [207] 214 | "", // [208] 215 | "", // [209] 216 | "", // [210] 217 | "", // [211] 218 | "", // [212] 219 | "", // [213] 220 | "", // [214] 221 | "", // [215] 222 | "", // [216] 223 | "", // [217] 224 | "", // [218] 225 | "OPEN_BRACKET", // [219] 226 | "BACK_SLASH", // [220] 227 | "CLOSE_BRACKET", // [221] 228 | "QUOTE", // [222] 229 | "", // [223] 230 | "META", // [224] 231 | "ALTGR", // [225] 232 | "", // [226] 233 | "WIN_ICO_HELP", // [227] 234 | "WIN_ICO_00", // [228] 235 | "", // [229] 236 | "WIN_ICO_CLEAR", // [230] 237 | "", // [231] 238 | "", // [232] 239 | "WIN_OEM_RESET", // [233] 240 | "WIN_OEM_JUMP", // [234] 241 | "WIN_OEM_PA1", // [235] 242 | "WIN_OEM_PA2", // [236] 243 | "WIN_OEM_PA3", // [237] 244 | "WIN_OEM_WSCTRL", // [238] 245 | "WIN_OEM_CUSEL", // [239] 246 | "WIN_OEM_ATTN", // [240] 247 | "WIN_OEM_FINISH", // [241] 248 | "WIN_OEM_COPY", // [242] 249 | "WIN_OEM_AUTO", // [243] 250 | "WIN_OEM_ENLW", // [244] 251 | "WIN_OEM_BACKTAB", // [245] 252 | "ATTN", // [246] 253 | "CRSEL", // [247] 254 | "EXSEL", // [248] 255 | "EREOF", // [249] 256 | "PLAY", // [250] 257 | "ZOOM", // [251] 258 | "", // [252] 259 | "PA1", // [253] 260 | "WIN_OEM_CLEAR", // [254] 261 | "" // [255] 262 | ]; 263 | -------------------------------------------------------------------------------- /examples/features/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | mod keycodes; 3 | 4 | use std::cell::RefCell; 5 | 6 | use json::JsonValue; 7 | 8 | use tinyweb::callbacks::create_async_callback; 9 | use tinyweb::router::{Page, Router}; 10 | use tinyweb::runtime::Runtime; 11 | use tinyweb::signals::Signal; 12 | use tinyweb::element::El; 13 | 14 | use tinyweb::invoke::*; 15 | 16 | const BUTTON_CLASSES: &[&str] = &["bg-blue-500", "hover:bg-blue-700", "text-white", "p-2", "rounded", "m-2"]; 17 | 18 | thread_local! { 19 | pub static ROUTER: RefCell = RefCell::new(Router::default()); 20 | } 21 | 22 | async fn fetch_json(method: &str, url: &str, body: Option) -> Result { 23 | let body = body.map(|s| s.dump()).unwrap_or_default(); 24 | let (callback_ref, future) = create_async_callback(); 25 | let request = r#" 26 | const options = { method: {}, headers: { 'Content-Type': 'application/json' }, body: p0 !== 'GET' ? {} : null }; 27 | fetch({}, options).then(r => r.json()).then(r => { {}(r) }) 28 | "#; 29 | Js::invoke(request, &[method.into(), body.into(), url.into(), callback_ref.into()]); 30 | let future_leak = Box::leak(Box::new(future)); 31 | let result_ref = future_leak.await; 32 | let result = Js::invoke("return JSON.stringify({})", &[result_ref.into()]).to_str().unwrap(); 33 | Js::deallocate(result_ref); 34 | json::parse(&result).map_err(|_| "Parse error".to_owned()) 35 | } 36 | 37 | fn page1() -> El { 38 | 39 | // signals 40 | let signal_key = Signal::new("-".to_owned()); 41 | let signal_count = Signal::new(0); 42 | let signal_time = Signal::new("-"); 43 | 44 | El::new("div") 45 | .once(move |_| { 46 | 47 | // add listener 48 | let body = Js::invoke("return document.querySelector({})", &["body".into()]).to_ref().unwrap(); 49 | 50 | El::from(&body).on("keydown", move |e| { 51 | let key_code = Js::invoke("return {}[{}]", &[e.into(), "keyCode".into()]).to_num().unwrap(); 52 | let key_name = keycodes::KEYBOARD_MAP[key_code as usize]; 53 | let text = format!("Pressed: {}", key_name); 54 | signal_key.set(text); 55 | }); 56 | 57 | // start timer 58 | Runtime::block_on(async move { 59 | loop { 60 | signal_time.set("⏰ tik"); 61 | Runtime::promise("window.setTimeout({},{})", move |c| vec![c.into(), 1_000.into()]).await; 62 | signal_time.set("⏰ tok"); 63 | Runtime::promise("window.setTimeout({},{})", move |c| vec![c.into(), 1_000.into()]).await; 64 | } 65 | }); 66 | 67 | }) 68 | .classes(&["m-2"]) 69 | .child(El::new("button").text("api").classes(&BUTTON_CLASSES).on_async("click", move |_| async { 70 | let url = format!("https://pokeapi.co/api/v2/pokemon/{}", 1); 71 | let result = fetch_json("GET", &url, None).await.unwrap(); 72 | let name = result["name"].as_str().unwrap(); 73 | Js::invoke("alert({})", &[name.into()]); 74 | })) 75 | .child(El::new("button").text("page 2").classes(&BUTTON_CLASSES).on("click", move |_| { 76 | ROUTER.with(|s| { s.borrow().navigate("/page2"); }); 77 | })) 78 | .child(El::new("br")) 79 | .child(El::new("button").text("add").classes(&BUTTON_CLASSES).on("click", move |_| { 80 | let count = signal_count.get() + 1; 81 | signal_count.set(count); 82 | })) 83 | .child(El::new("div").text("0").once(move |el| { 84 | signal_count.on(move |v| { Js::invoke("{}.innerHTML = {}", &[el.into(), v.to_string().into()]); }); 85 | })) 86 | .child(El::new("div").text("-").once(move |el| { 87 | signal_time.on(move |v| { Js::invoke("{}.innerHTML = {}", &[el.into(), v.into()]); }); 88 | })) 89 | .child(El::new("div").text("-").once(move |el| { 90 | signal_key.on(move |v| { Js::invoke("{}.innerHTML = {}", &[el.into(), v.into()]); }); 91 | })) 92 | } 93 | 94 | fn page2() -> El { 95 | El::new("div") 96 | .classes(&["m-2"]) 97 | .child(El::new("button").text("page 1").classes(&BUTTON_CLASSES).on("click", move |_| { 98 | ROUTER.with(|s| { s.borrow().navigate("/page1"); }); 99 | })) 100 | } 101 | 102 | #[no_mangle] 103 | pub fn main() { 104 | 105 | std::panic::set_hook(Box::new(|e| { Js::invoke("console.log({})", &[e.to_string().into()]); })); 106 | 107 | // init router 108 | let pages = &[Page::new("/page1", page1()), Page::new("/page2", page2())]; 109 | ROUTER.with(|s| { *s.borrow_mut() = Router::new("body", pages); }); 110 | } 111 | -------------------------------------------------------------------------------- /examples/minimal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimal" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type =["cdylib"] 8 | test = false 9 | 10 | [dependencies] 11 | tinyweb = { path = "../../src/rust" } 12 | 13 | -------------------------------------------------------------------------------- /examples/minimal/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | use tinyweb::invoke::*; 3 | 4 | #[no_mangle] 5 | pub fn main() { 6 | let body = Js::invoke("return document.querySelector({})", &["body".into()]).to_ref().unwrap(); 7 | Js::invoke("{}.innerHTML = {}", &[body.into(), "hello".into()]); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let wasmModule = {} 4 | 5 | const objects = new Map() 6 | if (typeof window !== 'undefined') window.objects = objects 7 | 8 | const getRandomId = () => Math.floor(Math.random() * Number(0xFFFFn)) 9 | 10 | const textEncoder = new TextEncoder() 11 | const textDecoder = new TextDecoder() 12 | 13 | const readParamsFromMemory = (ptr, len) => { 14 | 15 | const memory = new Uint8Array(wasmModule.instance.exports.memory.buffer) 16 | const params = new Uint8Array(memory.slice(ptr, ptr + len)) 17 | const dataView = new DataView(params.buffer) 18 | const values = [] 19 | let i = 0 20 | while (i < params.length) { 21 | if (params[i] === 0) { // undefined 22 | values.push(undefined) 23 | i += 1 24 | } else if (params[i] === 1) { // null 25 | values.push(null) 26 | i += 1 27 | } else if (params[i] === 2) { // f64 28 | values.push(dataView.getFloat64(i + 1, true)) 29 | i += 1 + 8 30 | } else if (params[i] === 3) { // big int 31 | values.push(dataView.getBigInt64(i + 1, true)) 32 | i += 1 + 8 33 | } else if (params[i] === 4) { // string 34 | const ptr = dataView.getInt32(i + 1, true) 35 | const len = dataView.getInt32(i + 1 + 4, true) 36 | values.push(textDecoder.decode(memory.subarray(ptr, ptr + len))) 37 | i += 1 + 4 + 4 38 | } else if (params[i] === 5) { // true 39 | values.push(true) 40 | i += 1 41 | } else if (params[i] === 6) { // false 42 | values.push(false) 43 | i += 1 44 | } else if (params[i] === 7) { // object ref 45 | const objectId = dataView.getUint32(i + 1, true) 46 | values.push(objects.get(objectId)) 47 | i += 1 + 4 48 | } else { 49 | throw new Error('Invalid parameter type') 50 | } 51 | } 52 | return values 53 | } 54 | 55 | const runFunction = (c_ptr, c_len, p_ptr, p_len) => { 56 | const memory = new Uint8Array(wasmModule.instance.exports.memory.buffer) 57 | const functionBody = textDecoder.decode(memory.subarray(c_ptr, c_ptr + c_len)) 58 | const _function = Function(`'use strict';return(${functionBody})`)() 59 | 60 | const values = readParamsFromMemory(p_ptr, p_len) 61 | return _function.call({}, ...values) 62 | } 63 | 64 | const getWasmImports = () => { 65 | 66 | const env = { 67 | __invoke (c_ptr, c_len, p_ptr, p_len) { 68 | const result = runFunction(c_ptr, c_len, p_ptr, p_len) 69 | if (typeof result === "undefined") { 70 | return (BigInt(0) << 32n) | BigInt(0) 71 | } else if (typeof result === "number") { 72 | const ptr = writeBufferToMemory(textEncoder.encode(result)) 73 | return (BigInt(1) << 32n) | BigInt(ptr) 74 | } else if (typeof result === "function") { 75 | 76 | const objectId = getRandomId() 77 | objects.set(objectId, result) 78 | 79 | return (BigInt(2) << 32n) | BigInt(objectId) 80 | } else if (typeof result === "object") { 81 | // because js has no primitive types for arrays 82 | if (result instanceof Uint8Array) { 83 | const ptr = writeBufferToMemory(new Uint8Array(result)) 84 | return (BigInt(3) << 32n) | BigInt(ptr) 85 | } else { 86 | 87 | const objectId = getRandomId() 88 | objects.set(objectId, result) 89 | 90 | return (BigInt(2) << 32n) | BigInt(objectId) 91 | } 92 | } else if (typeof result === "string") { 93 | const ptr = writeBufferToMemory(textEncoder.encode(result)) 94 | return (BigInt(4) << 32n) | BigInt(ptr) 95 | } else if (typeof result === "bigint") { 96 | return (BigInt(5) << 32n) | BigInt(result) 97 | } else if (typeof result === "boolean") { 98 | return (BigInt(6) << 32n) | BigInt(result) 99 | } else { 100 | throw new Error("Invalid result type") 101 | } 102 | }, 103 | __deallocate(objectId) { 104 | objects.delete(objectId) 105 | } 106 | } 107 | return { env } 108 | } 109 | 110 | const loadWasm = async () => { 111 | const imports = getWasmImports() 112 | const wasmScript = document.querySelector('script[type="application/wasm"]') 113 | const wasmBuffer = await fetch(wasmScript.src).then(r => r.arrayBuffer()) 114 | wasmModule = await WebAssembly.instantiate(wasmBuffer, imports) 115 | wasmModule.instance.exports.main() 116 | } 117 | 118 | const writeBufferToMemory = (buffer) => { 119 | const allocationId = wasmModule.instance.exports.create_allocation(buffer.length) 120 | const allocationPtr = wasmModule.instance.exports.get_allocation(allocationId) 121 | const memory = new Uint8Array(wasmModule.instance.exports.memory.buffer) 122 | memory.set(buffer, allocationPtr) 123 | return allocationId 124 | } 125 | 126 | const loadExports = () => { 127 | exports.wasmModule = wasmModule 128 | exports.writeBufferToMemory = writeBufferToMemory 129 | exports.readParamsFromMemory = readParamsFromMemory 130 | } 131 | 132 | if (typeof window !== 'undefined') { // load wasm (browser) 133 | document.addEventListener('DOMContentLoaded', loadWasm) 134 | } else { // load exports (nodejs) 135 | loadExports() 136 | } 137 | -------------------------------------------------------------------------------- /src/js/main.test.js: -------------------------------------------------------------------------------- 1 | const test = require('node:test') 2 | const assert = require('node:assert') 3 | 4 | const { readParamsFromMemory, writeBufferToMemory, wasmModule } = require('./main') 5 | 6 | // node src/js/main.test.js 7 | 8 | test('check read params', () => { 9 | 10 | const float64View = new DataView(new ArrayBuffer(8)) 11 | float64View.setFloat64(0, 42.42, true) 12 | const float64Array = new Uint8Array(float64View.buffer) 13 | 14 | const bigInt64View = new DataView(new ArrayBuffer(8)) 15 | bigInt64View.setBigInt64(0, 42n, true) 16 | const bigInt64Array = new Uint8Array(bigInt64View.buffer) 17 | 18 | const uint32View = new DataView(new ArrayBuffer(4)) 19 | uint32View.setUint32(0, 42, true) 20 | const uint32Array = new Uint8Array(uint32View.buffer) 21 | 22 | const testCases = [ 23 | {memory: [0], expected: [undefined]}, 24 | {memory: [1], expected: [null]}, 25 | {memory: [2, ...float64Array], expected: [42.42]}, 26 | {memory: [3, ...bigInt64Array], expected: [42n]}, 27 | {memory: [4, ...uint32Array, ...uint32Array], expected: ['']}, 28 | {memory: [5], expected: [true]}, 29 | {memory: [6], expected: [false]}, 30 | {memory: [7, ...uint32Array], expected: [undefined]}, 31 | ] 32 | for (const testCase of testCases) { 33 | wasmModule.instance = { exports: { memory: { buffer: testCase.memory } } } 34 | 35 | const result = readParamsFromMemory(0, testCase.memory.length) 36 | assert.deepStrictEqual(result, testCase.expected) 37 | } 38 | }) 39 | 40 | test('check write buffer', () => { 41 | 42 | const testCases = [ 43 | {memory: [], expected: 0}, 44 | ] 45 | const create_allocation = () => { return 0 } 46 | const get_allocation = () => { return 0 } 47 | for (const testCase of testCases) { 48 | 49 | const exports = { create_allocation, get_allocation, memory: { buffer: testCase.memory } } 50 | wasmModule.instance = { exports } 51 | 52 | const result = writeBufferToMemory(0, []) 53 | assert.deepStrictEqual(result, testCase.expected) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /src/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tinyweb" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | doc = false 8 | doctest = false 9 | 10 | [dev-dependencies] 11 | fantoccini = "0.21.1" 12 | serde_json = "1.0.127" 13 | tokio = { version = "1", features = ["full"] } 14 | -------------------------------------------------------------------------------- /src/rust/src/allocations.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::cell::RefCell; 3 | 4 | thread_local! { 5 | pub static ALLOCATIONS: RefCell>> = RefCell::new(Vec::new()); 6 | } 7 | 8 | #[no_mangle] 9 | pub fn create_allocation(size: usize) -> usize { 10 | ALLOCATIONS.with_borrow_mut(|s| { s.push(vec![0; size]); s.len() - 1 }) 11 | } 12 | 13 | #[no_mangle] 14 | pub fn get_allocation(allocation_id: usize) -> *const u8 { 15 | ALLOCATIONS.with_borrow(|s| s.get(allocation_id).unwrap().as_ptr()) 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn test_allocation() { 24 | 25 | // test string 26 | let text = "hello"; 27 | let id = create_allocation(1); 28 | ALLOCATIONS.with_borrow_mut(|s| { s[id as usize] = text.as_bytes().to_vec(); }); 29 | let allocation_data = ALLOCATIONS.with_borrow(|s| s.get(id as usize).unwrap().to_owned()); 30 | let memory_text = String::from_utf8(allocation_data).unwrap(); 31 | assert_eq!(memory_text, text); 32 | 33 | // test vec 34 | let vec = vec![1, 2]; 35 | let id = create_allocation(1); 36 | ALLOCATIONS.with_borrow_mut(|s| { s[id as usize] = vec.clone(); }); 37 | let memory_vec = ALLOCATIONS.with_borrow(|s| s.get(id as usize).unwrap().to_owned()); 38 | assert_eq!(memory_vec, vec); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rust/src/callbacks.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::runtime::{FutureState, FutureTask}; 3 | use crate::invoke::{Js, ObjectRef}; 4 | 5 | use std::collections::HashMap; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | 9 | thread_local! { 10 | pub static CALLBACK_HANDLERS: RefCell>> = Default::default(); 11 | } 12 | 13 | pub fn create_callback(mut handler: impl FnMut(ObjectRef) + 'static) -> ObjectRef { 14 | let code = r#" 15 | const handler = (e) => { 16 | const handlerObjectId = getRandomId(); 17 | objects.set(handlerObjectId, e); 18 | wasmModule.instance.exports.handle_callback(objectId, handlerObjectId); 19 | }; 20 | const objectId = getRandomId(); 21 | objects.set(objectId, handler); 22 | return objectId; 23 | "#; 24 | let object_id = Js::invoke(code, &[]).to_num().unwrap(); 25 | let function_ref = ObjectRef::new(object_id as u32); 26 | let cb = move |value| { handler(value); }; 27 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().insert(function_ref.clone(), Box::new(cb)); }); 28 | function_ref 29 | } 30 | 31 | #[no_mangle] 32 | pub fn handle_callback(callback_id: u32, param_id: u32) { 33 | 34 | let object_ref = ObjectRef::new(param_id as u32); 35 | let callback_ref = ObjectRef::new(callback_id); 36 | 37 | CALLBACK_HANDLERS.with(|s| { 38 | let handler = s.borrow_mut().get_mut(&callback_ref).unwrap() as *mut Box; 39 | unsafe { (*handler)(object_ref) } 40 | }); 41 | 42 | Js::deallocate(callback_ref); 43 | } 44 | 45 | pub fn create_async_callback() -> (ObjectRef, FutureTask) { 46 | let future = FutureTask { state: Rc::new(RefCell::new(FutureState::Init)) }; 47 | let state_clone = future.state.clone(); 48 | let callback_ref = create_callback(move |e| { 49 | let mut future_state = state_clone.borrow_mut(); 50 | if let FutureState::Pending(ref mut waker) = &mut *future_state { waker.to_owned().wake(); } 51 | *future_state = FutureState::Ready(e); 52 | }); 53 | return (callback_ref, future); 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | 59 | use std::cell::RefCell; 60 | use std::rc::Rc; 61 | 62 | use super::*; 63 | 64 | #[test] 65 | fn test_callback() { 66 | 67 | // add listener 68 | let has_run = Rc::new(RefCell::new(false)); 69 | let has_run_clone = has_run.clone(); 70 | create_callback(move |_| { *has_run_clone.borrow_mut() = true; }); 71 | 72 | // simulate callback 73 | let function_ref = ObjectRef::new(0); 74 | handle_callback(*function_ref, 0); 75 | assert_eq!(*has_run.borrow(), true); 76 | 77 | // remove listener 78 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().remove(&function_ref); }); 79 | let count = CALLBACK_HANDLERS.with(|s| s.borrow().len()); 80 | assert_eq!(count, 0); 81 | } 82 | 83 | #[test] 84 | fn test_future_callback() { 85 | 86 | // add listener 87 | let (function_ref, future) = create_async_callback(); 88 | 89 | // simulate callback 90 | handle_callback(*function_ref, 0); 91 | crate::runtime::Runtime::block_on(async move { future.await; }); 92 | 93 | // remove listener 94 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().remove(&function_ref); }); 95 | let count = CALLBACK_HANDLERS.with(|s| s.borrow().len()); 96 | assert_eq!(count, 0); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/rust/src/element.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::future::Future; 3 | use std::cell::RefCell; 4 | use std::rc::Rc; 5 | 6 | use crate::invoke::{Js, ObjectRef}; 7 | use crate::runtime::Runtime; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub struct El { pub element: ObjectRef, pub callbacks: RefCell> } 11 | 12 | impl El { 13 | pub fn new(tag: &str) -> Self { 14 | let el = Js::invoke("return document.createElement({})", &[tag.into()]).to_ref().unwrap(); 15 | Self { element: el, callbacks: RefCell::new(vec![]) } 16 | } 17 | pub fn from(el: &ObjectRef) -> Self { 18 | Self { element: el.to_owned(), callbacks: RefCell::new(vec![]) } 19 | } 20 | pub fn mount(&self, parent: &ObjectRef) { 21 | Js::invoke("{}.appendChild({})", &[parent.into(), self.element.into()]); 22 | } 23 | pub fn unmount(&self) { 24 | let mut c = self.callbacks.borrow_mut(); 25 | c.iter().for_each(|p| { 26 | crate::callbacks::CALLBACK_HANDLERS.with(|s| { let _ = s.borrow_mut().remove(p).unwrap(); }); 27 | }); 28 | c.clear(); 29 | } 30 | pub fn attr(self, name: &str, value: &str) -> Self { 31 | Js::invoke("{}.setAttribute({},{})", &[self.element.into(), name.into(), value.into()]); 32 | self 33 | } 34 | pub fn attr_fn(self, name: &str, value: &str, cb: impl Fn() -> bool + 'static) -> Self { 35 | if cb() { 36 | Js::invoke("{}.setAttribute({},{})", &[self.element.into(), name.into(), value.into()]); 37 | } 38 | self 39 | } 40 | pub fn classes(self, classes: &[&str]) -> Self { 41 | classes.iter().for_each(|&c| { Js::invoke("{}.classList.add({})", &[self.element.into(), c.into()]); }); 42 | self 43 | } 44 | pub fn child(self, child: Self) -> Self { 45 | Js::invoke("{}.appendChild({})", &[self.element.into(), child.element.into()]); 46 | self 47 | } 48 | pub fn children(self, children: &[Self]) -> Self { 49 | Js::invoke("{}.innerHTML = {}", &[self.element.into(), "".into()]); 50 | for child in children { 51 | Js::invoke("{}.appendChild({})", &[self.element.into(), child.element.into()]); 52 | } 53 | self 54 | } 55 | pub fn once(self, cb: impl FnMut(ObjectRef) + 'static) -> Self { 56 | 57 | let cb = Rc::new(RefCell::new(cb)); 58 | cb.borrow_mut()(self.element); 59 | 60 | self 61 | } 62 | pub fn once_async>(self, cb: impl FnMut(ObjectRef) -> Fut + 'static) -> Self { 63 | 64 | let cb = Rc::new(RefCell::new(cb)); 65 | 66 | Runtime::block_on(async move { 67 | cb.borrow_mut()(self.element).await; 68 | }); 69 | 70 | self 71 | } 72 | pub fn on(self, event: &str, cb: impl FnMut(ObjectRef) + 'static) -> Self { 73 | 74 | let function_ref = crate::callbacks::create_callback(cb); 75 | let code = &format!("{{}}.addEventListener('{}',{{}})", event); 76 | Js::invoke(code, &[self.element.into(), function_ref.into()]); 77 | 78 | self.callbacks.borrow_mut().push(function_ref); 79 | 80 | self 81 | } 82 | pub fn on_async>(self, event: &str, cb: impl FnMut(ObjectRef) -> Fut + 'static) -> Self { 83 | 84 | let cb = Rc::new(RefCell::new(cb)); 85 | let cb_async = move |e| { 86 | let cb = cb.clone(); 87 | 88 | Runtime::block_on(async move { cb.borrow_mut()(e).await; }); 89 | Js::deallocate(e); 90 | }; 91 | 92 | let function_ref = crate::callbacks::create_callback(cb_async); 93 | let code = &format!("{{}}.addEventListener('{}',{{}})", event); 94 | Js::invoke(code, &[self.element.into(), function_ref.into()]); 95 | 96 | self.callbacks.borrow_mut().push(function_ref); 97 | 98 | self 99 | } 100 | pub fn text(self, text: &str) -> Self { 101 | 102 | let el = Js::invoke("return document.createTextNode({})", &[text.into()]).to_ref().unwrap(); 103 | Js::invoke("{}.appendChild({})", &[self.element.into(), el.into()]); 104 | 105 | self 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | 112 | use super::*; 113 | 114 | #[test] 115 | fn test_element() { 116 | 117 | let el = El::new("div").classes(&[]) 118 | .child(El::new("button").text("button 1")) 119 | .child(El::new("button").text("button 2")); 120 | assert_eq!(el, El::from(&ObjectRef::new(0))); 121 | 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/rust/src/invoke.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::ops::Deref; 3 | 4 | #[cfg(not(test))] 5 | extern "C" { 6 | fn __invoke(c_ptr: *const u8, c_len: u32, p_ptr: *const u8, p_len: u32) -> u64; 7 | fn __deallocate(object_id: u32); 8 | } 9 | 10 | #[cfg(test)] 11 | unsafe fn __invoke(_c_ptr: *const u8, _c_len: u32, _p_ptr: *const u8, _p_len: u32) -> u64 { 0 } 12 | #[cfg(test)] 13 | unsafe fn __deallocate(_object_id: u32) {} 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 16 | pub struct ObjectRef(u32); 17 | 18 | impl ObjectRef { 19 | pub fn new(object_id: u32) -> Self { 20 | Self (object_id) 21 | } 22 | } 23 | 24 | impl Deref for ObjectRef { 25 | type Target = u32; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | // NOTE: Numbers in Javascript are represented by 64-bits floats 33 | // https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type 34 | #[derive(Debug)] 35 | pub enum JsValue { 36 | Undefined, 37 | Null, 38 | Number(f64), 39 | BigInt(i64), 40 | Str(String), 41 | Bool(bool), 42 | Ref(ObjectRef), 43 | Buffer(Vec), 44 | } 45 | 46 | impl From<&str> for JsValue { fn from(s: &str) -> Self { Self::Str(s.to_string()) } } 47 | impl From for JsValue { fn from(s: String) -> Self { Self::Str(s) } } 48 | impl From for JsValue { fn from(n: f64) -> Self { Self::Number(n) } } 49 | impl From for JsValue { fn from(n: f32) -> Self { Self::Number(n as f64) } } 50 | impl From for JsValue { fn from(n: u32) -> Self { Self::Number(n as f64) } } 51 | impl From for JsValue { fn from(n: u64) -> Self { Self::Number(n as f64) } } 52 | impl From for JsValue { fn from(n: i32) -> Self { Self::Number(n as f64) } } 53 | impl From for JsValue { fn from(n: i64) -> Self { Self::Number(n as f64) } } 54 | impl From for JsValue { fn from(n: bool) -> Self { Self::Bool(n) } } 55 | impl From for JsValue { fn from(s: ObjectRef) -> Self { Self::Ref(s) } } 56 | impl From<&ObjectRef> for JsValue { fn from(s: &ObjectRef) -> Self { Self::Ref(s.to_owned()) } } 57 | impl From> for JsValue { fn from(s: Vec) -> Self { Self::Buffer(s) } } 58 | 59 | // pub use JsValue::*; 60 | 61 | impl JsValue { 62 | 63 | // layout: type (1 byte) - data (var length) 64 | pub fn serialize(&self) -> Vec { 65 | match self { 66 | JsValue::Undefined => vec![0], 67 | JsValue::Null => vec![1], 68 | JsValue::Number(i) => [vec![2], i.to_le_bytes().to_vec()].concat(), 69 | JsValue::BigInt(i) => [vec![3], i.to_le_bytes().to_vec()].concat(), 70 | JsValue::Str(s) => [vec![4], (s.as_ptr() as u32).to_le_bytes().to_vec(), s.len().to_le_bytes().to_vec()].concat(), 71 | JsValue::Bool(b) => vec![if *b { 5 } else { 6 }], 72 | JsValue::Ref(i) => [vec![7], i.0.to_le_bytes().to_vec()].concat(), 73 | JsValue::Buffer(b) => [vec![8], b.to_owned()].concat(), 74 | } 75 | } 76 | 77 | pub fn deserialize(r_type: u32, r_value: u32) -> Self { 78 | match r_type { 79 | 0 => JsValue::Undefined, 80 | 1 => { 81 | let allocation_data = crate::allocations::ALLOCATIONS.with_borrow_mut(|s| s.remove(r_value as usize)); 82 | let value = String::from_utf8_lossy(&allocation_data); 83 | JsValue::Number(value.parse::().unwrap() as f64) 84 | }, 85 | 2 => JsValue::Ref(ObjectRef(r_value)), 86 | 3 => { 87 | JsValue::Buffer(crate::allocations::ALLOCATIONS.with_borrow_mut(|s| s.remove(r_value as usize))) 88 | }, 89 | 4 => { 90 | let allocation_data = crate::allocations::ALLOCATIONS.with_borrow_mut(|s| s.remove(r_value as usize)); 91 | JsValue::Str(String::from_utf8_lossy(&allocation_data).into()) 92 | }, 93 | 5 => JsValue::BigInt(r_value as i64), 94 | 6 => JsValue::Bool(if r_value == 1 { true } else { false }), 95 | 96 | _ => unreachable!(), 97 | } 98 | } 99 | 100 | pub fn to_bool(&self) -> Result { 101 | match &self { 102 | JsValue::Bool(b) => Ok(b.to_owned()), 103 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 104 | #[cfg(test)] _ => Ok(true), 105 | } 106 | } 107 | 108 | pub fn to_str(&self) -> Result { 109 | match &self { 110 | JsValue::Str(s) => Ok(s.to_string()), 111 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 112 | #[cfg(test)] _ => Ok("".to_string()), 113 | } 114 | } 115 | 116 | pub fn to_num(&self) -> Result { 117 | match &self { 118 | JsValue::Number(s) => Ok(s.to_owned()), 119 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 120 | #[cfg(test)] _ => Ok(0.into()), 121 | } 122 | } 123 | pub fn to_ref(&self) -> Result { 124 | match &self { 125 | JsValue::Ref(s) => Ok(s.to_owned()), 126 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 127 | #[cfg(test)] _ => Ok(ObjectRef(0)), 128 | } 129 | } 130 | pub fn to_buffer(&self) -> Result, String> { 131 | match &self { 132 | JsValue::Buffer(s) => Ok(s.to_owned()), 133 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 134 | #[cfg(test)] _ => Ok(vec![]), 135 | } 136 | } 137 | 138 | pub fn to_bigint(&self) -> Result { 139 | match &self { 140 | JsValue::BigInt(s) => Ok(s.to_owned()), 141 | #[cfg(not(test))] _ => Err("Invalid type".to_string()), 142 | #[cfg(test)] _ => Ok(0.into()), 143 | } 144 | } 145 | } 146 | 147 | 148 | pub struct Js {} 149 | 150 | impl Js { 151 | fn __code(code: &str, params: &[JsValue]) -> String { 152 | 153 | let mut code_params = String::from(code); 154 | 155 | let params_names = params.iter().enumerate().map(|(i, _)| "p".to_owned() + &i.to_string()).collect::>(); 156 | for param_name in ¶ms_names { 157 | if let Some(pos) = code_params.find("{}") { 158 | code_params.replace_range(pos..pos + 2, param_name); 159 | } 160 | } 161 | format!("function({}) {{ {} }}", params_names.join(","), code_params) 162 | } 163 | pub fn invoke<'a>(code: &'a str, params: &[JsValue]) -> JsValue { 164 | let code = Self::__code(code, params); 165 | let params = params.iter().flat_map(JsValue::serialize).collect::>(); 166 | let r_packed = unsafe { __invoke(code.as_ptr(), code.len() as u32, params.as_ptr(), params.len() as u32) }; 167 | let r_type = (r_packed >> 32) as u32; 168 | let r_value = (r_packed & 0xFFFFFFFF) as u32; 169 | JsValue::deserialize(r_type, r_value) 170 | } 171 | pub fn deallocate(object_id: ObjectRef) { 172 | unsafe { __deallocate(*object_id) }; 173 | } 174 | } 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | 179 | use super::*; 180 | 181 | fn cs(s: &str) -> String { 182 | s.chars().filter(|c| !c.is_whitespace()).collect::() 183 | } 184 | 185 | #[test] 186 | fn test_params() { 187 | 188 | // undefined 189 | assert_eq!(JsValue::Undefined.serialize(), vec![0]); 190 | 191 | // null 192 | assert_eq!(JsValue::Null.serialize(), vec![1]); 193 | 194 | // number 195 | assert_eq!(JsValue::Number(42.into()).serialize(), [vec![2], 42f64.to_le_bytes().to_vec()].concat()); 196 | 197 | // bigint 198 | assert_eq!(JsValue::BigInt(42).serialize(), [vec![3], 42u64.to_le_bytes().to_vec()].concat()); 199 | 200 | // string 201 | let text = "hello".to_owned(); 202 | let text_ptr = text.as_ptr() as u32; 203 | let text_len = text.len() as u64; 204 | let expected = [vec![4], text_ptr.to_le_bytes().to_vec(), text_len.to_le_bytes().to_vec()].concat(); 205 | assert_eq!(JsValue::Str(text).serialize(), expected); 206 | 207 | // bool 208 | assert_eq!(JsValue::Bool(true).serialize(), vec![5]); 209 | assert_eq!(JsValue::Bool(false).serialize(), vec![6]); 210 | 211 | // object ref 212 | assert_eq!(JsValue::Ref(ObjectRef(42)).serialize(), [vec![7], 42u32.to_le_bytes().to_vec()].concat()); 213 | 214 | // buffer 215 | assert_eq!(JsValue::Buffer(vec![1, 2, 3]).serialize(), [vec![8], vec![1, 2, 3]].concat()); 216 | 217 | } 218 | 219 | #[test] 220 | fn test_code() { 221 | // prompt 222 | let code = Js::__code("return prompt({},{})", &["a".into(), "b".into()]); 223 | let expected_code = "function(p0,p1){ return prompt(p0,p1) }"; 224 | assert_eq!(cs(&code), cs(&expected_code)); 225 | 226 | // console log 227 | let code = Js::__code("console.log({})", &["a".into()]); 228 | let expected_code = "function(p0){ console.log(p0) }"; 229 | assert_eq!(cs(&code), cs(&expected_code)); 230 | 231 | // alert 232 | let code = Js::__code("alert({})", &["a".into()]); 233 | let expected_code = "function(p0){ alert(p0) }"; 234 | assert_eq!(cs(&code), cs(&expected_code)); 235 | 236 | // set attribute 237 | let code = Js::__code("{}.setAttribute({},{})", &[ObjectRef(0).into(), "a".into(), "b".into()]); 238 | let expected_code = "function(p0,p1,p2){ p0.setAttribute(p1, p2) }"; 239 | assert_eq!(cs(&code), cs(&expected_code)); 240 | 241 | // append child 242 | let code = Js::__code("{}.appendChild({})", &[ObjectRef(0).into(), ObjectRef(0).into()]); 243 | let expected_code = "function(p0,p1){ p0.appendChild(p1) }"; 244 | assert_eq!(cs(&code), cs(&expected_code)); 245 | 246 | // add class 247 | let code = Js::__code("{}.classList.add({})", &[ObjectRef(0).into(), "a".into()]); 248 | let expected_code = "function(p0,p1){ p0.classList.add(p1) }"; 249 | assert_eq!(cs(&code), cs(&expected_code)); 250 | 251 | // set property 252 | let code = Js::__code("{}[{}] = {}", &[ObjectRef(0).into(), "a".into(), "a".into()]); 253 | let expected_code = "function(p0,p1,p2){ p0[p1] = p2 }"; 254 | assert_eq!(cs(&code), cs(&expected_code)); 255 | 256 | // set inner html 257 | let code = Js::__code("{}.innerHTML = {}", &[ObjectRef(0).into(), "a".into()]); 258 | let expected_code = "function(p0,p1){ p0.innerHTML = p1 }"; 259 | assert_eq!(cs(&code), cs(&expected_code)); 260 | 261 | // history push state 262 | // NOTE: {} is parsed as the first parameter 263 | let code = Js::__code("window.history.pushState({ },{},{})", &["a".into(), "b".into()]); 264 | let expected_code = "function(p0,p1){ window.history.pushState({ },p0,p1) }"; 265 | assert_eq!(cs(&code), cs(&expected_code)); 266 | 267 | // location pathname 268 | let code = Js::__code("return window.location.pathname", &[]); 269 | let expected_code = "function() { return window.location.pathname }"; 270 | assert_eq!(cs(&code), cs(&expected_code)); 271 | 272 | // get property string 273 | let code = Js::__code("return {}[{}]", &[ObjectRef(0).into(), "b".into()]); 274 | let expected_code = "function(p0,p1){ return p0[p1] }"; 275 | assert_eq!(cs(&code), cs(&expected_code)); 276 | 277 | // prompt dialog 278 | let code = Js::__code("return prompt({},{})", &["a".into(), "b".into()]); 279 | let expected_code = "function(p0,p1){ return prompt(p0,p1) }"; 280 | assert_eq!(cs(&code), cs(&expected_code)); 281 | 282 | // random number 283 | let code = Js::__code("return Math.random()", &[]); 284 | let expected_code = "function(){ return Math.random() }"; 285 | assert_eq!(cs(&code), cs(&expected_code)); 286 | 287 | // get property 288 | let code = Js::__code("return {}[{}]", &[ObjectRef(0).into(), "a".into()]); 289 | let expected_code = "function(p0,p1){ return p0[p1] }"; 290 | assert_eq!(cs(&code), cs(&expected_code)); 291 | 292 | // query selector 293 | let code = Js::__code("return document.querySelector({})", &["a".into()]); 294 | let expected_code = "function(p0){ return document.querySelector(p0) }"; 295 | assert_eq!(cs(&code), cs(&expected_code)); 296 | 297 | // create element 298 | let code = Js::__code("return document.createElement({})", &["a".into()]); 299 | let expected_code = "function(p0){ return document.createElement(p0) }"; 300 | assert_eq!(cs(&code), cs(&expected_code)); 301 | 302 | // create text node 303 | let code = Js::__code("return document.createTextNode({})", &["a".into()]); 304 | let expected_code = "function(p0){ return document.createTextNode(p0) }"; 305 | assert_eq!(cs(&code), cs(&expected_code)); 306 | 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod callbacks; 3 | pub mod allocations; 4 | pub mod runtime; 5 | pub mod invoke; 6 | 7 | pub mod signals; 8 | pub mod element; 9 | pub mod router; 10 | 11 | // Use: crate::println!("{}", 42); 12 | #[macro_export] 13 | macro_rules! println { 14 | ($fmt:expr) => { Js::invoke("console.log({})", &[format!($fmt).into()]); }; 15 | ($fmt:expr, $($arg:tt)*) => { Js::invoke("console.log({})", &[format!($fmt, $($arg)*).into()]); }; 16 | } 17 | 18 | // Web browser specification 19 | // https://github.com/w3c/webref 20 | 21 | // Count LOC (excluding tests) 22 | // ``` 23 | // git ls-files ':(glob)src/rust/src/**' | xargs cat | sed '/#\[test\]/,/}/d' | wc -l 24 | // ``` 25 | 26 | // List files 27 | // ``` 28 | // git ls-files ':(glob)src/rust/src/**' | xargs wc -l | sort -r 29 | // ``` 30 | -------------------------------------------------------------------------------- /src/rust/src/router.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use std::collections::HashMap; 4 | 5 | use crate::invoke::{Js, ObjectRef}; 6 | use crate::element::El; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Page { pub path: String, pub element: El, pub title: Option } 10 | 11 | impl Page { 12 | pub fn new(path: &str, element: El) -> Self { 13 | Self { path: path.to_owned(), element, title: None } 14 | } 15 | pub fn title(mut self, title: String) -> Self { 16 | self.title = Some(title); 17 | self 18 | } 19 | } 20 | 21 | #[derive(Debug, Default)] 22 | pub struct Router { pub root: Option, pub pages: HashMap:: } 23 | 24 | impl Router { 25 | pub fn new(root: &str, pages: &[Page]) -> Self { 26 | let body = Js::invoke("return document.querySelector({})", &[root.into()]).to_ref().unwrap(); 27 | let pathname = Js::invoke("return window.location.pathname", &[]).to_str().unwrap(); 28 | let page = pages.iter().find(|&s| *s.path == pathname).unwrap_or(&pages[0]); 29 | page.element.mount(&body); 30 | 31 | let mut default_page = pages.first().cloned().unwrap(); 32 | default_page.path = "/".to_owned(); 33 | 34 | let mut pages = pages.iter().map(|p| (p.path.clone(), p.to_owned())).collect::>(); 35 | pages.push((default_page.path.clone(), default_page.to_owned())); 36 | Self { pages: HashMap::from_iter(pages), root: Some(body) } 37 | } 38 | pub fn navigate(&self, route: &str) { 39 | 40 | // unmount page 41 | let pathname = Js::invoke("return window.location.pathname", &[]).to_str().unwrap(); 42 | let (_, current_page) = self.pages.iter().find(|&(s, _)| *s == pathname).unwrap(); 43 | current_page.element.unmount(); 44 | 45 | // set html 46 | let body = self.root.as_ref().unwrap(); 47 | Js::invoke("{}.innerHTML = {}", &[body.into(), "".into()]); 48 | 49 | // mount new page 50 | let page = self.pages.get(route).unwrap(); 51 | page.element.mount(&body); 52 | 53 | // push state 54 | let page_str = page.title.to_owned().unwrap_or_default(); 55 | Js::invoke("window.history.pushState({ },{},{})", &[page_str.into(), route.into()]); 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/rust/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::RefCell, 3 | future::Future, 4 | mem::ManuallyDrop, 5 | pin::Pin, 6 | rc::Rc, 7 | task::{Context, Poll, RawWaker, RawWakerVTable, Waker} 8 | }; 9 | 10 | use crate::callbacks::{create_async_callback, create_callback}; 11 | use crate::invoke::{Js, JsValue, ObjectRef}; 12 | 13 | pub enum FutureState { Init, Pending(Waker), Ready(ObjectRef) } 14 | pub struct FutureTask { pub state: Rc> } 15 | 16 | pub struct Runtime {} 17 | 18 | type FutureRc = Rc>>>>; 19 | 20 | impl Future for FutureTask { 21 | type Output = ObjectRef; 22 | 23 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 24 | 25 | let mut future_state = self.state.borrow_mut(); 26 | match &*future_state { 27 | FutureState::Ready(result) => { 28 | Poll::Ready(result.to_owned()) 29 | }, 30 | _ => { 31 | *future_state = FutureState::Pending(cx.waker().to_owned()); 32 | Poll::Pending 33 | } 34 | } 35 | } 36 | } 37 | 38 | impl Drop for FutureTask { 39 | fn drop(&mut self) { 40 | match *self.state.borrow_mut() { 41 | FutureState::Ready(id) => Js::deallocate(id), 42 | _ => {} 43 | } 44 | } 45 | } 46 | 47 | impl Runtime { 48 | 49 | fn poll(future_rc: &FutureRc) { 50 | let waker = Self::waker(&future_rc); 51 | let waker_forget = ManuallyDrop::new(waker); 52 | let context = &mut Context::from_waker(&waker_forget); 53 | let _poll = future_rc.borrow_mut().as_mut().poll(context); 54 | } 55 | 56 | // https://rust-lang.github.io/async-book/02_execution/03_wakeups.html 57 | fn waker(future_rc: &FutureRc) -> Waker { 58 | 59 | fn clone_fn(ptr: *const ()) -> RawWaker { 60 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) }; 61 | let _ = ManuallyDrop::new(future).clone(); 62 | RawWaker::new(ptr, waker_vtable::()) 63 | } 64 | fn wake_fn(ptr: *const ()) { 65 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) }; 66 | let function_ref = create_callback(move |e| { Runtime::poll(&future); Js::deallocate(e); }); 67 | Js::invoke("window.setTimeout({},0)", &[function_ref.into()]); 68 | } 69 | fn drop_fn(ptr: *const ()) { 70 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) }; 71 | drop(future); 72 | } 73 | fn waker_vtable() -> &'static RawWakerVTable { 74 | &RawWakerVTable::new(clone_fn::, wake_fn::, wake_fn::, drop_fn::) 75 | } 76 | let waker = RawWaker::new(&**future_rc as *const _ as *const (), waker_vtable::()); 77 | unsafe { Waker::from_raw(waker) } 78 | } 79 | 80 | pub fn block_on(future: impl Future + 'static) { 81 | Self::poll(&Rc::new(RefCell::new(Box::pin(future)))); 82 | } 83 | 84 | pub fn promise Vec>(code: &str, params_fn: F) -> FutureTask { 85 | let (callback_ref, future) = create_async_callback(); 86 | Js::invoke(code, ¶ms_fn(callback_ref)); 87 | future 88 | } 89 | } 90 | 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | 95 | use super::*; 96 | 97 | #[test] 98 | fn test_await() { 99 | 100 | // create future 101 | let future = FutureTask { state: Rc::new(RefCell::new(FutureState::Init)) }; 102 | let future_state = future.state.clone(); 103 | assert_eq!(matches!(*future_state.borrow(), FutureState::Init), true); 104 | 105 | // set to ready 106 | *future_state.borrow_mut() = FutureState::Ready(ObjectRef::new(42)); 107 | let _object_ref = ObjectRef::new(42); 108 | assert_eq!(matches!(*future_state.borrow(), FutureState::Ready(_object_ref)), true); 109 | 110 | // block on future 111 | let has_run = Rc::new(RefCell::new(ObjectRef::new(42))); 112 | let has_run_clone = has_run.clone(); 113 | Runtime::block_on(async move { *has_run_clone.borrow_mut() = future.await; }); 114 | assert_eq!(*has_run.borrow(), ObjectRef::new(42)); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/rust/src/signals.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::{cell::RefCell, rc::Rc}; 3 | 4 | #[derive(Clone)] 5 | pub struct Signal { 6 | value: Rc>, 7 | // NOTE: since `FnMut` can mutate state it has to go behind a smart pointer 8 | subscribers: Rc>>>> 9 | } 10 | 11 | impl Signal { 12 | pub fn new(value: T) -> &'static Self { 13 | let signal = Self { value: Rc::new(RefCell::new(value)), subscribers: Default::default(), }; 14 | &*Box::leak(Box::new(signal)) 15 | } 16 | pub fn get(&self) -> T { 17 | self.value.borrow().clone() 18 | } 19 | pub fn set(&self, new_value: T) { 20 | // store value 21 | *self.value.borrow_mut() = new_value; 22 | 23 | // trigger effects 24 | self.subscribers.borrow_mut().iter().for_each(|f| { f.borrow_mut()(); }); 25 | } 26 | pub fn on(&self, mut cb: impl FnMut(T) + 'static) { 27 | 28 | // get callback 29 | let signal_clone = self.clone(); 30 | let cb_ref = Rc::new(RefCell::new(move || { cb(signal_clone.get()); })); 31 | 32 | // store callback 33 | self.subscribers.borrow_mut().push(cb_ref.to_owned()); 34 | 35 | // trigger once 36 | cb_ref.borrow_mut()(); 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | 43 | use super::*; 44 | 45 | #[test] 46 | fn test_signals() { 47 | 48 | // create signal 49 | let logs: Rc>> = Default::default(); 50 | let signal = Signal::new(10); 51 | 52 | // create effects 53 | let logs_clone = logs.clone(); 54 | signal.on(move |v| { logs_clone.borrow_mut().push(v); }); 55 | let logs_clone = logs.clone(); 56 | signal.on(move |v| { logs_clone.borrow_mut().push(v); }); 57 | 58 | // update signal 59 | signal.set(20); 60 | signal.set(30); 61 | 62 | // check logs 63 | assert_eq!(*logs.borrow(), vec![10, 10, 20, 20, 30, 30]); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/rust/tests/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::process::{Command, Stdio}; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Duration; 5 | use std::env::temp_dir; 6 | use std::path::PathBuf; 7 | 8 | use fantoccini::wd::Capabilities; 9 | use fantoccini::{ClientBuilder, Locator}; 10 | 11 | pub const WASM_TRIPLET: &str = "wasm32-unknown-unknown"; 12 | 13 | fn get_pid_on_port(port: u16) -> Option { 14 | let output = Command::new("lsof").args(&["-ti", format!(":{port}").as_str()]).output().unwrap(); 15 | let stdout_opt = if output.stdout.is_empty() { None } else { Some(output.stdout) }; 16 | stdout_opt.map(|o| std::str::from_utf8(&o).map(|p| p.trim().parse().unwrap()).unwrap()) 17 | } 18 | 19 | fn setup_temp_project() -> PathBuf { 20 | // get paths 21 | let temp_dir = temp_dir(); 22 | let cwd = std::env::current_dir().unwrap(); 23 | let project_path = cwd.parent().unwrap().parent().unwrap(); 24 | 25 | // build wasm 26 | let p = Command::new("cargo").args(["build", "-p", "minimal", "--target", WASM_TRIPLET]).output().unwrap(); 27 | assert!(p.status.success()); 28 | 29 | // copy wasm 30 | let wasm_path = project_path.join("target").join(WASM_TRIPLET).join("debug").join("minimal.wasm"); 31 | std::fs::copy(wasm_path, temp_dir.join("client.wasm")).unwrap(); 32 | 33 | // copy js 34 | let js_path = project_path.join("src").join("js").join("main.js"); 35 | std::fs::copy(js_path, temp_dir.join("main.js")).unwrap(); 36 | 37 | // copy html 38 | let html = r#""#; 39 | std::fs::write(temp_dir.join("index.html"), html).unwrap(); 40 | 41 | temp_dir 42 | } 43 | 44 | // lsof -i tcp:4444 && kill -9 ${PID} 45 | #[tokio::test] 46 | async fn test_wasm() -> Result<(), fantoccini::error::CmdError> { 47 | 48 | // start daemon 49 | let lock = Arc::new(Mutex::new(None)); 50 | let lock_clone = lock.clone(); 51 | std::thread::spawn(move || { 52 | 53 | let pid = get_pid_on_port(4444); 54 | if let Some(pid) = pid { 55 | Command::new("kill").arg(format!("{}", pid)).status().unwrap(); 56 | } 57 | 58 | let child = Command::new("geckodriver").stderr(Stdio::null()).spawn().unwrap(); 59 | lock_clone.lock().map(|mut s| { *s = Some(child); }).unwrap(); 60 | }); 61 | std::thread::sleep(Duration::from_millis(1_000)); 62 | 63 | // open browser 64 | let mut client_builder = ClientBuilder::native(); 65 | let mut caps = Capabilities::new(); 66 | caps.insert("moz:firefoxOptions".to_string(), serde_json::json!({ "args": ["--headless"] })); 67 | client_builder.capabilities(caps); 68 | let client = client_builder.connect("http://localhost:4444").await.unwrap(); 69 | 70 | // prepare project 71 | let project_dir = setup_temp_project(); 72 | 73 | // load html 74 | let index_html = "/index.html"; 75 | let url = format!("file://{}{}", project_dir.to_str().unwrap(), index_html); 76 | client.goto(&url).await?; 77 | 78 | std::thread::sleep(Duration::from_millis(1_000)); 79 | 80 | // check body 81 | let body = client.find(Locator::Css("body")).await?; 82 | let body_str = body.html(true).await?; 83 | assert!(body_str.contains("hello")); 84 | 85 | // stop browser 86 | client.close().await?; 87 | 88 | // stop daemon 89 | lock.lock().map(|mut s| { 90 | let child = s.as_mut().unwrap(); 91 | child.kill().unwrap(); 92 | }).unwrap(); 93 | 94 | Ok(()) 95 | 96 | } --------------------------------------------------------------------------------