├── .cargo └── config.toml ├── .editorconfig ├── .github └── workflows │ ├── build-and-test.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── assets ├── winterjs-logo-black.png └── winterjs-logo-white.png ├── benchmark ├── .gitignore ├── README.md ├── bun-simple.js ├── complex.js ├── node-simple.js ├── simple.js └── worker.capnp ├── build-debug.sh ├── build-single-threaded.sh ├── build.rs ├── build.sh ├── docs ├── References.md └── spidermonkey_cookbook.cpp ├── examples ├── echo │ ├── app.yaml │ ├── app │ │ └── app.js │ └── wasmer.toml ├── hello-world.js └── http-echo │ ├── README.md │ ├── app.yaml │ ├── src │ └── index.js │ └── wasmer.toml ├── flake.lock ├── flake.nix ├── package-lock.json ├── package.json ├── rust-toolchain ├── shell.nix ├── src ├── builtins │ ├── cache │ │ ├── cache_storage.rs │ │ └── mod.rs │ ├── core │ │ ├── core.js │ │ └── mod.rs │ ├── crypto │ │ ├── mod.rs │ │ └── subtle │ │ │ ├── algorithm │ │ │ ├── hmac.rs │ │ │ ├── md5.rs │ │ │ ├── mod.rs │ │ │ └── sha.rs │ │ │ ├── crypto_key.rs │ │ │ ├── jwk.rs │ │ │ └── mod.rs │ ├── internal_js_modules.rs │ ├── internal_js_modules │ │ ├── __types │ │ │ └── __winterjs_core_.d.ts │ │ ├── __winterjs_internal │ │ │ ├── base64-js.js │ │ │ └── ieee754.js │ │ ├── node │ │ │ ├── async_hooks.js │ │ │ ├── async_hooks.ts │ │ │ └── buffer.js │ │ └── tsconfig.json │ ├── js_globals.rs │ ├── js_globals │ │ ├── event.js │ │ ├── event.ts │ │ ├── readable-stream.js │ │ └── tsconfig.json │ ├── mod.rs │ ├── navigator.rs │ ├── performance.rs │ └── process.rs ├── main.rs ├── request_handlers │ ├── cloudflare │ │ ├── context.rs │ │ ├── env.rs │ │ ├── mod.rs │ │ └── routes.rs │ ├── mod.rs │ ├── service_workers │ │ ├── event_listener.rs │ │ ├── fetch_event.rs │ │ └── mod.rs │ └── wintercg.rs ├── runners │ ├── event_loop_stream.rs │ ├── exec.rs │ ├── inline.rs │ ├── mod.rs │ ├── request_loop.rs │ ├── request_queue.rs │ ├── single.rs │ └── watch.rs ├── server.rs └── sm_utils.rs ├── test-suite ├── Cargo.lock ├── Cargo.toml ├── js-test-app │ ├── .gitignore │ ├── package.json │ ├── pnpm-lock.yaml │ ├── rollup.config.js │ └── src │ │ ├── main.js │ │ ├── test-files │ │ ├── 1-hello.js │ │ ├── 10-atob-btoa.js │ │ ├── 11-fetch.js │ │ ├── 11.1-fetch-body.js │ │ ├── 12-streams.js │ │ ├── 12.1-transform-stream.js │ │ ├── 12.2-text-encoder-stream.js │ │ ├── 12.3-text-decoder-stream.js │ │ ├── 13-performance.js │ │ ├── 14-form-data.js │ │ ├── 15-timers.js │ │ ├── 16-crypto.js │ │ ├── 16.1-crypto-hmac.js │ │ ├── 16.2-crypto-sha.js │ │ ├── 17-cache.js │ │ ├── 18-event.js │ │ ├── 19-abort.js │ │ ├── 2-blob.js │ │ ├── 2.1-file.js │ │ ├── 3-headers.js │ │ ├── 4-request.js │ │ ├── 5-response.js │ │ ├── 6-text-encoder.js │ │ ├── 7-text-decoder.js │ │ ├── 8-url.js │ │ ├── 8.1-search-params.js │ │ └── 9-wait-until.js │ │ └── test-utils.js ├── src │ ├── lib.rs │ └── main.rs └── winterjs-tests.toml ├── tests ├── complex.js ├── edge-template.js ├── fetch.js ├── promise.js ├── simple.js └── wrangler.js └── wasmer.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | linker = "lld-link.exe" 3 | 4 | [target.i686-pc-windows-msvc] 5 | linker = "lld-link.exe" 6 | 7 | [target.aarch64-pc-windows-msvc] 8 | linker = "lld-link.exe" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build-and-test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | metadata: [ 16 | { 17 | name: "Ubuntu (x86 native)", 18 | os: ubuntu-latest, 19 | bin-path: target/release-compact/winterjs, 20 | artifact-name: winterjs-linux-x86, 21 | target: native, 22 | }, 23 | { 24 | name: "macOS (arm native)", 25 | os: macos-latest, 26 | bin-path: target/release-compact/winterjs, 27 | artifact-name: winterjs-macos-arm, 28 | target: native, 29 | }, 30 | { 31 | name: "macOS (x86 native)", 32 | os: macos-13, 33 | bin-path: target/release-compact/winterjs, 34 | artifact-name: winterjs-macos-x86, 35 | target: native, 36 | }, 37 | { 38 | name: "Ubuntu (wasix)", 39 | os: ubuntu-latest, 40 | bin-path: target/wasm32-wasmer-wasi/release/winterjs.wasm, 41 | artifact-name: winterjs-wasix, 42 | target: wasix, 43 | wasix-toolchain-release-asset: rust-toolchain-x86_64-unknown-linux-gnu.tar.gz 44 | }, 45 | # { 46 | # name: "macOS (wasix)", 47 | # os: macos-latest, 48 | # bin-path: target/release-compact/winterjs, 49 | # artifact-name: winterjs-macos-wasix, 50 | # target: wasix, 51 | # wasix-toolchain-release-asset: rust-toolchain-aarch64-apple-darwin.tar.gz 52 | # } 53 | ] 54 | name: Build and Test - ${{ matrix.metadata.name }} 55 | runs-on: ${{ matrix.metadata.os }} 56 | steps: 57 | - name: Check out 58 | uses: actions/checkout@v3 59 | with: 60 | submodules: "recursive" 61 | 62 | - name: OS Setup (Ubuntu) 63 | if: ${{ matrix.metadata.os == 'ubuntu-latest' }} 64 | run: | 65 | sudo apt-get update 66 | sudo apt-get install -y build-essential python3.12 python3-distutils-extra llvm-15 libclang-dev clang-15 wabt 67 | npm i -g wasm-opt pnpm concurrently 68 | sudo rm /usr/bin/clang 69 | sudo rm /usr/bin/clang++ 70 | sudo ln -s /usr/bin/clang-15 /usr/bin/clang 71 | sudo ln -s /usr/bin/clang++-15 /usr/bin/clang++ 72 | sudo ln -s /usr/bin/llvm-ar-15 /usr/bin/llvm-ar 73 | sudo ln -s /usr/bin/llvm-nm-15 /usr/bin/llvm-nm 74 | sudo ln -s /usr/bin/llvm-ranlib-15 /usr/bin/llvm-ranlib 75 | sudo ln -s /usr/bin/llvm-objdump-15 /usr/bin/llvm-objdump 76 | 77 | - name: OS Setup (macOS) 78 | if: ${{ startsWith(matrix.metadata.os, 'macos-') }} 79 | run: | 80 | brew install wabt llvm@15 81 | [ -d "/opt/homebrew" ] && echo PATH="/opt/homebrew/opt/llvm@15/bin:$PATH" >> $GITHUB_ENV 82 | [ ! -d "/opt/homebrew" ] && echo PATH="/usr/local/opt/llvm@15/bin:$PATH" >> $GITHUB_ENV 83 | npm i -g wasm-opt pnpm concurrently 84 | 85 | - name: Tool Versions 86 | run: | 87 | echo clang 88 | clang -v 89 | echo '####################' 90 | echo llvm-ar 91 | llvm-ar -V 92 | echo '####################' 93 | echo llvm-nm 94 | llvm-nm -V 95 | echo '####################' 96 | echo llvm-ranlib 97 | llvm-ranlib -v 98 | echo '####################' 99 | echo wasm-opt 100 | wasm-opt --version 101 | echo '####################' 102 | echo wasm-strip 103 | wasm-strip --version 104 | echo '####################' 105 | echo python 106 | python3.12 -V 107 | 108 | - name: Install Rust 109 | uses: dtolnay/rust-toolchain@master 110 | with: 111 | toolchain: "1.76" 112 | components: "clippy,rustfmt" 113 | 114 | - name: Setup Wasmer 115 | if: ${{ matrix.metadata.target == 'wasix' }} 116 | uses: wasmerio/setup-wasmer@v3.1 117 | 118 | - name: Download wasix-libc 119 | if: ${{ matrix.metadata.target == 'wasix' }} 120 | uses: dsaltares/fetch-gh-release-asset@1.1.2 121 | with: 122 | repo: wasix-org/rust 123 | file: wasix-libc.tar.gz 124 | target: sysroot/wasix-libc.tar.gz 125 | 126 | - name: Unpack wasix-libc 127 | if: ${{ matrix.metadata.target == 'wasix' }} 128 | run: | 129 | cd sysroot 130 | tar xzf wasix-libc.tar.gz 131 | 132 | - name: Download wasix toolchain 133 | if: ${{ matrix.metadata.target == 'wasix' }} 134 | uses: dsaltares/fetch-gh-release-asset@1.1.2 135 | with: 136 | repo: wasix-org/rust 137 | file: ${{ matrix.metadata.wasix-toolchain-release-asset }} 138 | target: wasix-rust-toolchain/toolchain.tar.gz 139 | 140 | - name: Install wasix toolchain 141 | if: ${{ matrix.metadata.target == 'wasix' }} 142 | run: | 143 | cd wasix-rust-toolchain 144 | tar xzf toolchain.tar.gz 145 | chmod +x bin/* 146 | chmod +x lib/rustlib/*/bin/* 147 | chmod +x lib/rustlib/*/bin/gcc-ld/* 148 | rustup toolchain link wasix . 149 | 150 | - name: Build native 151 | if: ${{ matrix.metadata.target == 'native' }} 152 | run: cargo build --profile release-compact 153 | 154 | - name: Build wasix 155 | if: ${{ matrix.metadata.target == 'wasix' }} 156 | run: | 157 | export WASI_SYSROOT=$(pwd)/sysroot/wasix-libc/sysroot32 158 | bash build.sh 159 | 160 | - name: Archive build 161 | uses: actions/upload-artifact@v4 162 | with: 163 | name: ${{ matrix.metadata.artifact-name }} 164 | path: ${{ matrix.metadata.bin-path }} 165 | 166 | - name: build test suite JS app 167 | run: | 168 | cd test-suite/js-test-app 169 | pnpm i 170 | pnpm run build 171 | 172 | - name: Run API test suite (wasix) 173 | # note: we're counting on wasmer compiling and running WinterJS faster 174 | # that cargo builds the test-suite app. This may not be the case forever. 175 | if: ${{ matrix.metadata.target == 'wasix' }} 176 | run: | 177 | conc --kill-others --success "command-1" \ 178 | "wasmer run . --net --mapdir /app:./test-suite/js-test-app/dist -- serve /app/bundle.js" \ 179 | "sleep 10 && cd test-suite && cargo run" 180 | echo All tests are passing! 🎉 181 | 182 | - name: Run API test suite (native) 183 | # note: we're counting on wasmer compiling and running WinterJS faster 184 | # that cargo builds the test-suite app. This may not be the case forever. 185 | if: ${{ matrix.metadata.target == 'native' }} 186 | run: | 187 | conc --kill-others --success "command-1" \ 188 | "./target/release-compact/winterjs serve ./test-suite/js-test-app/dist/bundle.js" \ 189 | "sleep 10 && cd test-suite && cargo run" 190 | echo All tests are passing! 🎉 191 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-and-test: 10 | uses: "./.github/workflows/build-and-test.yml" 11 | 12 | release: 13 | runs-on: ubuntu-latest 14 | needs: build-and-test 15 | steps: 16 | - name: Setup Wasmer 17 | uses: wasmerio/setup-wasmer@v3.1 18 | 19 | - name: Check out 20 | uses: actions/checkout@v3 21 | with: 22 | submodules: "recursive" 23 | 24 | - name: Download build artifacts 25 | uses: actions/download-artifact@v3 26 | with: 27 | path: build-artifacts 28 | 29 | - name: Publish 30 | run: | 31 | TAG_NAME=${{github.ref_name}} 32 | VERSION_NUMBER=${TAG_NAME#v} 33 | echo Publishing version $VERSION_NUMBER 34 | 35 | if ! grep -q "version = ['\"]$VERSION_NUMBER['\"]" wasmer.toml; then 36 | echo Tagged version must match version in wasmer.toml 37 | exit -1 38 | fi 39 | 40 | if ! grep -q "version = ['\"]$VERSION_NUMBER['\"]" Cargo.toml; then 41 | echo Tagged version must match version in Cargo.toml 42 | exit -1 43 | fi 44 | 45 | mkdir -p target/wasm32-wasmer-wasi/release 46 | mv build-artifacts/winterjs-wasix/winterjs.wasm target/wasm32-wasmer-wasi/release/winterjs.wasm 47 | 48 | wasmer publish --registry="wasmer.io" --token=${{ secrets.WAPM_PROD_TOKEN }} --non-interactive 49 | 50 | echo WinterJS version $VERSION_NUMBER was published successfully 🎉 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc 2 | **/target 3 | .direnv 4 | .envrc 5 | __pycache__ 6 | 7 | *.pyc 8 | *.o 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | .vscode 14 | .DS_Store 15 | 16 | **/.wrangler 17 | 18 | **/node_modules -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winterjs" 3 | description = "The JavaScript runtime that brings JavaScript to Wasmer Edge." 4 | version = "1.1.5" 5 | repository = "https://github.com/wasmerio/winterjs/" 6 | license = "MIT" 7 | edition = "2021" 8 | 9 | [dependencies] 10 | lazy_static = "1.4.0" 11 | anyhow = "1.0.75" 12 | hyper = { version = "=0.14.28", features = [ 13 | "server", 14 | "http1", 15 | "tcp", 16 | ], git = "https://github.com/wasix-org/hyper", branch = "v0.14.28" } 17 | tracing = "0.1.37" 18 | tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt"] } 19 | serde_derive = "1.0.164" 20 | serde = "1.0.164" 21 | serde_json = "1.0.97" 22 | bytes = { version = "1.5.0", features = ["serde"] } 23 | once_cell = "1.18.0" 24 | rustls = { git = "https://github.com/wasix-org/rustls.git", branch = "v0.22.2", version = "=0.22.2" } 25 | hyper-rustls = { version = "=0.25.0", git = "https://github.com/wasix-org/hyper-rustls.git", branch = "v0.25.0" } 26 | h2 = { version = "=0.3.23", git = "https://github.com/wasix-org/h2.git", branch = "v0.3.23" } 27 | futures = "0.3.28" 28 | http = "0.2.9" 29 | form_urlencoded = "1.2.0" 30 | 31 | static-web-server = { git = "https://github.com/wasix-org/static-web-server", rev = "87bb6804a7e60db08399f8f7f22bb10bf028a489" } 32 | 33 | ion = { package = "ion", features = [ 34 | "macros", 35 | ], git = "https://github.com/wasmerio/spiderfire.git" } 36 | ion-proc = { package = "ion-proc", git = "https://github.com/wasmerio/spiderfire.git" } 37 | runtime = { package = "runtime", git = "https://github.com/wasmerio/spiderfire.git", features = ["fetch"]} 38 | modules = { package = "modules", git = "https://github.com/wasmerio/spiderfire.git" } 39 | mozjs = { version = "0.14.1", git = "https://github.com/wasmerio/mozjs.git", branch = "wasi-gecko", features = [ 40 | "streams", 41 | ] } 42 | mozjs_sys = { version = "0.68.2", git = "https://github.com/wasmerio/mozjs.git", branch = "wasi-gecko" } 43 | 44 | # for mozjs 45 | libc = { version = "=0.2.152", git = "https://github.com/wasix-org/libc.git", branch = "v0.2.152" } 46 | 47 | # libc = "=0.2.139" 48 | 49 | # NOTE: We need to pin and replace some dependencies to achieve wasix compatibility. 50 | tokio = { version = "=1.35.1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"] } 51 | parking_lot = { version = "=0.12.1", features = ["nightly"] } 52 | url = "2.4.1" 53 | base64 = "0.21.4" 54 | async-trait = "0.1.74" 55 | uuid = { version = "1.5.0", features = ["v4"] } 56 | rand = "0.8.5" 57 | rand_core = "0.6.4" 58 | sha1 = "0.10.6" 59 | sha2 = "0.10.8" 60 | md5 = "0.7.0" 61 | clap = { version = "4.4.7", features = ["derive", "env"] } 62 | strum = { version = "0.25.0", features = ["derive"] } 63 | hmac = { version = "0.12.1", features = ["std"] } 64 | include_dir = "0.7.3" 65 | dyn-clone = "1.0.16" 66 | dyn-clonable = "0.9.0" 67 | self_cell = "1.0.3" 68 | glob-match = "0.2.1" 69 | sys-locale = "0.3.1" 70 | 71 | [target.'cfg(not(target_os = "wasi"))'.dependencies] 72 | ctrlc = "3.4.2" 73 | 74 | [patch.crates-io] 75 | hyper-rustls = { git = "https://github.com/wasix-org/hyper-rustls.git", branch = "v0.25.0" } 76 | socket2 = { git = "https://github.com/wasix-org/socket2.git", branch = "v0.5.5" } 77 | libc = { git = "https://github.com/wasix-org/libc.git", branch = "v0.2.152" } 78 | mio = { git = "https://github.com/wasix-org/mio.git", branch = "v0.8.9" } 79 | tokio = { git = "https://github.com/wasix-org/tokio.git", branch = "wasix-1.35.1" } 80 | rustls = { git = "https://github.com/wasix-org/rustls.git", branch = "v0.22.2" } 81 | hyper = { git = "https://github.com/wasix-org/hyper.git", branch = "v0.14.28" } 82 | h2 = { git = "https://github.com/wasix-org/h2", branch = "v0.3.23" } 83 | parking_lot_core = { git = "https://github.com/wasix-org/parking_lot.git", branch = "v0.12.1" } 84 | 85 | [profile.release-compact] 86 | inherits = "release" 87 | opt-level = "z" 88 | lto = true 89 | codegen-units = 1 90 | panic = "abort" 91 | strip = true 92 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | image = "ghcr.io/servo/cross-aarch64-unknown-linux-gnu:main" 3 | 4 | [target.armv7-unknown-linux-gnueabihf] 5 | image = "ghcr.io/servo/cross-armv7-unknown-linux-gnueabihf:main" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Wasmer, Inc. and its affiliates. 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Wasmer logo 6 | 7 | 8 |
9 | 10 | WinterJS is *blazing-fast* JavaScript server that runs Service Workers scripts according to the [Winter Community Group specification](https://wintercg.org/). 11 | 12 | **WinterJS is able to handle up to 100,000 reqs/s in a single laptop** (see [Benchmark](./benchmark)). 13 | 14 | ---- 15 | 16 | > Note: WinterJS is not officially endorsed by WinterCG, despite sharing "Winter" in their name. There are many [runtimes supporting WinterCG](https://runtime-keys.proposal.wintercg.org/), WinterJS being one among those. 17 | 18 | ## Running WinterJS with Wasmer 19 | 20 | The WinterJS server is published in Wasmer as [`wasmer/winterjs`](https://wasmer.io/wasmer/winterjs). 21 | 22 | You can run the HTTP server locally with: 23 | 24 | ```shell 25 | wasmer run wasmer/winterjs --net --mapdir=tests:tests tests/simple.js 26 | ``` 27 | 28 | Where `simple.js` is: 29 | 30 | ```js 31 | addEventListener('fetch', (req) => { 32 | req.respondWith(new Response('hello')); 33 | }); 34 | ``` 35 | 36 | ## Building from source 37 | 38 | WinterJS needs to build SpiderMonkey from source as part of its own build process. 39 | Please follow the steps outlined here to make sure you are ready to build SpiderMonkey: https://github.com/wasmerio/mozjs/blob/master/README.md. 40 | 41 | You also need to do this before installing WinterJS with `cargo install`, which builds WinterJS from the source instead of downloading pre-built binaries. 42 | 43 | Also, when building WinterJS in debug mode, you need to have NodeJS and npm installed and run `npm install` beforehand. 44 | This is required since debug builds of WinterJS also build the TypeScript code in `src/builtins/internal_js_modules`. 45 | Release builds use the existing, pre-compiled `*.js` sources in that directory. 46 | This was done to simplify installing WinterJS with `cargo install`. 47 | If you update the TypeScript sources, make sure to update the `*.js` sources by building at least once in debug mode and commit the updated `*.js` files. 48 | 49 | Once you can build SpiderMonkey, you simply need to run `cargo build` as usual to build WinterJS itself. 50 | 51 | ## Running WinterJS Natively 52 | 53 | You can install WinterJS natively with: 54 | 55 | ``` 56 | cargo install --git https://github.com/wasmerio/winterjs winterjs 57 | ``` 58 | 59 | Once you have WinterJS installed, you can simply do: 60 | 61 | ```shell 62 | winterjs tests/simple.js 63 | ``` 64 | 65 | And then access the server in https://localhost:8080/ 66 | 67 | # How WinterJS works 68 | 69 | WinterJS is powered by [SpiderMonkey](https://spidermonkey.dev/), [Spiderfire](https://github.com/Redfire75369/spiderfire) and [hyper](https://hyper.rs/) 70 | to bring a new level of awesomeness to your Javascript apps. 71 | 72 | WinterJS is using the [WASIX](https://wasix.org) standard to compile to WebAssembly. Please note that compiling to WASIX is currently a complex process. We recommend using precompiled versions from [`wasmer/winterjs`](https://wasmer.io/wasmer/winterjs), but please open an issue if you need to compile to WASIX locally. 73 | 74 | ## Limitations 75 | 76 | WinterJS is fully compliant with the WinterCG spec, although the runtime itself is still a work in progress. 77 | For more information, see the API Compatibility section below. 78 | 79 | # WinterCG API Compatibility 80 | 81 | This section will be updated as APIs are added/fixed. 82 | If an API is missing from this section, that means that it is still not implemented. 83 | 84 | You can check a more detailed list here: https://runtime-compat.unjs.io/ 85 | 86 | The following words are used to describe the status of an API: 87 | 88 | * ✅ Stable - The API is implemented and fully compliant with the spec. This does not account for potential undiscovered implementation errors in the native code. 89 | * 🔶 Partial - The API is implemented but not fully compliant with the spec and/or there are known limitations. 90 | * ❌ Pending - The API is not implemented yet. 91 | 92 | |API|Status|Notes| 93 | |:-:|:-:|:--| 94 | |`console`|✅ Stable| 95 | |`fetch`|✅ Stable| 96 | |`URL`|✅ Stable| 97 | |`URLSearchParams`|✅ Stable| 98 | |`Request`|✅ Stable| 99 | |`Headers`|✅ Stable| 100 | |`Response`|✅ Stable| 101 | |`Blob`|✅ Stable| 102 | |`File`|✅ Stable| 103 | |`FormData`|✅ Stable| 104 | |`TextDecoder`|✅ Stable| 105 | |`TextDecoderStream`|✅ Stable| 106 | |`TextEncoder`|✅ Stable| 107 | |`TextEncoderStream`|✅ Stable| 108 | |`ReadableStream` and supporting types|✅ Stable| 109 | |`WritableStream` and supporting types|✅ Stable| 110 | |`TransformStream` and supporting types|🔶 Partial|Back-pressure is not implemented 111 | |`atob`|✅ Stable| 112 | |`btoa`|✅ Stable| 113 | |`performance.now()`|✅ Stable| 114 | |`performance.timeOrigin`|✅ Stable| 115 | |`crypto`|✅ Stable| 116 | |`crypto.subtle`|🔶 Partial|Only HMAC, MD5 and SHA algorithms are supported 117 | 118 | # Other supported APIs 119 | 120 | The following (non-WinterCG) APIs are implemented and accessible in WinterJS: 121 | 122 | |API|Status|Notes| 123 | |:-:|:-:|:--| 124 | |[Service Workers Caches API](https://www.w3.org/TR/service-workers/#cache-objects)|✅ Stable|Accessible via `caches`. `caches.default` (similar to [Cloudflare workers](https://developers.cloudflare.com/workers/runtime-apis/cache/#accessing-cache)) is also available.
The current implementation is memory-backed, and cached responses will *not* persist between multiple runs of WinterJS. 125 | -------------------------------------------------------------------------------- /assets/winterjs-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/winterjs/b74e8c70f8106c6be790cb33dea5409d89d42b37/assets/winterjs-logo-black.png -------------------------------------------------------------------------------- /assets/winterjs-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/winterjs/b74e8c70f8106c6be790cb33dea5409d89d42b37/assets/winterjs-logo-white.png -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .wrangler 3 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | This benchmarks are done in a MacBook Pro M3 Max laptop with 64 GB of RAM, on Feb 28th, 2024. 4 | 5 | This benchmark compares: 6 | * [`workerd`](#workerd): Cloudflare's Service Worker server powered by V8 (repo: https://github.com/cloudflare/workerd) 7 | * [WinterJS Native](#winterjs-native): WinterJS running natively 8 | * [Bun](#bun): Bun (basic http server replicating similar behavior) 9 | * [Node](#node): Node (basic http server replicating similar behavior) 10 | * [WinterJS WASIX](#winterjs-wasix): WinterJS running in Wasmer via WASIX 11 | * [`wrangler`](#wrangler): Cloudflare's Service Worker powered by Node (repo: https://github.com/cloudflare/workers-sdk) 12 | 13 | 14 | > Note: this benchmarks focuses on running a simple workload [`simple.js`](./simple.js). There's also the [`complex.js`](./complex.js) file, which does Server Side Rendering using React. 15 | 16 | 17 | ## Workerd 18 | 19 | Using latest release binary: https://github.com/cloudflare/workerd/releases/tag/v1.20231030.0 20 | 21 | Running the server: 22 | 23 | ``` 24 | $ ./workerd-darwin-arm64 serve ./worker.capnp 25 | ``` 26 | 27 | And then: 28 | 29 | ```bash 30 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8080 31 | Running 10s test @ http://127.0.0.1:8080 32 | 12 threads and 400 connections 33 | Thread Stats Avg Stdev Max +/- Stdev 34 | Latency 14.55ms 22.15ms 116.50ms 81.86% 35 | Req/Sec 3.32k 1.52k 9.65k 69.58% 36 | 396904 requests in 10.04s, 31.42MB read 37 | Socket errors: connect 155, read 110, write 0, timeout 0 38 | Requests/sec: 39522.93 39 | Transfer/sec: 3.13MB 40 | ``` 41 | 42 | ## WinterJS (Native) 43 | 44 | Running the server: 45 | 46 | ``` 47 | $ cargo run --release -- ./simple.js 48 | ``` 49 | 50 | Benchmarking: 51 | ``` 52 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8080 53 | Running 10s test @ http://127.0.0.1:8080 54 | 12 threads and 400 connections 55 | Thread Stats Avg Stdev Max +/- Stdev 56 | Latency 11.70ms 89.98ms 1.52s 98.29% 57 | Req/Sec 12.66k 6.49k 46.35k 58.80% 58 | 1517019 requests in 10.10s, 173.61MB read 59 | Socket errors: connect 155, read 108, write 0, timeout 29 60 | Requests/sec: 150175.14 61 | Transfer/sec: 17.19MB 62 | ``` 63 | 64 | 65 | ## Bun 66 | 67 | > Note: Bun does run another equivalent script to `simple.js` (`bun-simple.js`), since Bun does not support WinterCG natively. 68 | 69 | Running the server: 70 | 71 | ``` 72 | $ bun ./bun-simple.js 73 | ``` 74 | 75 | Benchmarking: 76 | 77 | ``` 78 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8080 79 | Running 10s test @ http://127.0.0.1:8080 80 | 12 threads and 400 connections 81 | Thread Stats Avg Stdev Max +/- Stdev 82 | Latency 2.05ms 401.85us 8.29ms 78.81% 83 | Req/Sec 9.83k 5.40k 17.65k 45.96% 84 | 1186158 requests in 10.10s, 135.75MB read 85 | Socket errors: connect 155, read 57, write 0, timeout 0 86 | Requests/sec: 117418.44 87 | Transfer/sec: 13.44MB 88 | ``` 89 | 90 | 91 | ## Node 92 | 93 | > Note: Node does run another equivalent script to `simple.js` (`node-simple.js`), since Node does not support WinterCG natively. 94 | 95 | Running the server: 96 | 97 | ``` 98 | $ node ./node-simple.js 99 | ``` 100 | 101 | Benchmarking: 102 | 103 | ``` 104 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8080 105 | Running 10s test @ http://127.0.0.1:8080 106 | 12 threads and 400 connections 107 | Thread Stats Avg Stdev Max +/- Stdev 108 | Latency 3.91ms 10.03ms 294.68ms 99.16% 109 | Req/Sec 6.25k 2.00k 11.33k 73.92% 110 | 747990 requests in 10.02s, 122.69MB read 111 | Socket errors: connect 155, read 306, write 0, timeout 0 112 | Requests/sec: 74615.22 113 | Transfer/sec: 12.24MB 114 | ``` 115 | 116 | ## WinterJS (WASIX) 117 | 118 | Running the server: 119 | 120 | ``` 121 | $ wasmer run wasmer/winterjs --mapdir=/app:. --net -- /app/simple.js 122 | ``` 123 | 124 | Benchmarking: 125 | 126 | ``` 127 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8080 128 | Running 10s test @ http://127.0.0.1:8080 129 | 12 threads and 400 connections 130 | Thread Stats Avg Stdev Max +/- Stdev 131 | Latency 11.22ms 8.97ms 168.70ms 87.08% 132 | Req/Sec 1.05k 526.90 2.99k 73.00% 133 | 125542 requests in 10.03s, 14.37MB read 134 | Socket errors: connect 155, read 271, write 0, timeout 0 135 | Requests/sec: 12519.78 136 | Transfer/sec: 1.43MB 137 | ``` 138 | 139 | ## Wrangler 140 | 141 | Running the server: 142 | 143 | ```bash 144 | $ npx wrangler@3.15.0 dev ./simple.js 145 | ``` 146 | 147 | Benchmarking (please note that the port is in `8787`): 148 | 149 | ```bash 150 | $ wrk -t12 -c400 -d10s http://127.0.0.1:8787 151 | Running 10s test @ http://127.0.0.1:8787 152 | 12 threads and 400 connections 153 | Thread Stats Avg Stdev Max +/- Stdev 154 | Latency 94.97ms 118.83ms 401.80ms 83.00% 155 | Req/Sec 166.86 189.25 810.00 77.68% 156 | 19461 requests in 10.08s, 1.56MB read 157 | Socket errors: connect 155, read 259, write 1, timeout 0 158 | Requests/sec: 1930.96 159 | Transfer/sec: 158.63KB 160 | ``` 161 | -------------------------------------------------------------------------------- /benchmark/bun-simple.js: -------------------------------------------------------------------------------- 1 | const server = Bun.serve({ 2 | port: 8080, 3 | fetch(request) { 4 | return new Response('hello'); 5 | }, 6 | }); 7 | 8 | console.log(`Server running at ${server.url}`); 9 | -------------------------------------------------------------------------------- /benchmark/node-simple.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const server = http.createServer((req, res) => { 4 | res.writeHead(200, {'Content-Type': 'text/plain'}); 5 | res.end('hello'); 6 | }); 7 | 8 | server.listen(8080, () => { 9 | console.log('Server running at http://localhost:8080/'); 10 | }); 11 | -------------------------------------------------------------------------------- /benchmark/simple.js: -------------------------------------------------------------------------------- 1 | 2 | addEventListener('fetch', (req) => { 3 | req.respondWith(new Response('hello')); 4 | }); 5 | -------------------------------------------------------------------------------- /benchmark/worker.capnp: -------------------------------------------------------------------------------- 1 | using Workerd = import "/workerd/workerd.capnp"; 2 | 3 | const config :Workerd.Config = ( 4 | services = [ 5 | (name = "main", worker = .mainWorker), 6 | ], 7 | 8 | sockets = [ 9 | # Serve HTTP on port 8080. 10 | ( name = "http", 11 | address = "*:8080", 12 | http = (), 13 | service = "main" 14 | ), 15 | ] 16 | ); 17 | 18 | const mainWorker :Workerd.Worker = ( 19 | serviceWorkerScript = embed "./simple.js", 20 | compatibilityDate = "2023-02-28", 21 | # Learn more about compatibility dates at: 22 | # https://developers.cloudflare.com/workers/platform/compatibility-dates/ 23 | ); 24 | -------------------------------------------------------------------------------- /build-debug.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -euo pipefail 4 | set -x 5 | 6 | # Note: cargo-wasix automatically runs wasm-opt with -O2, which makes the resulting binary unusable. 7 | # Instead, we use the toolchain to build (cargo +wasix instead of cargo wasix) and optimize manually. 8 | cargo +wasix build --target wasm32-wasmer-wasi 9 | mv target/wasm32-wasmer-wasi/debug/winterjs.wasm x.wasm 10 | wasm-opt x.wasm -o target/wasm32-wasmer-wasi/debug/winterjs.wasm -O1 --enable-bulk-memory --enable-threads --enable-reference-types --no-validation --asyncify 11 | rm x.wasm 12 | wasm-strip target/wasm32-wasmer-wasi/debug/winterjs.wasm -------------------------------------------------------------------------------- /build-single-threaded.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -euo pipefail 4 | set -x 5 | 6 | # Note: cargo-wasix automatically runs wasm-opt with -O2, which makes the resulting binary unusable. 7 | # Instead, we use the toolchain to build (cargo +wasix instead of cargo wasix) and optimize manually. 8 | cargo +wasix build --target wasm32-wasmer-wasi -r 9 | mv target/wasm32-wasmer-wasi/release/winterjs.wasm x.wasm 10 | # In single-thread-only builds, we skip --asyncify 11 | wasm-opt x.wasm -o target/wasm32-wasmer-wasi/release/winterjs-st.wasm -O1 --enable-bulk-memory --enable-reference-types --no-validation 12 | rm x.wasm 13 | wasm-strip target/wasm32-wasmer-wasi/release/winterjs-st.wasm -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | // We want to let people install WinterJS from source, so we can't have 5 | // a dependency on TSC at all times. The assumption here is that whoever 6 | // updates TS sources for builtin modules will at least run WinterJS 7 | // once in debug mode, and the JS scripts will be updated and pushed. 8 | let profile = std::env::var("PROFILE").unwrap(); 9 | if profile == "debug" { 10 | let builtins_dir = std::env::current_dir().unwrap().join("src/builtins"); 11 | 12 | let dir = builtins_dir.join("internal_js_modules"); 13 | assert!(Command::new("npx") 14 | .arg("tsc") 15 | .current_dir(dir) 16 | .output() 17 | .unwrap() 18 | .status 19 | .success()); 20 | 21 | let dir = builtins_dir.join("js_globals"); 22 | assert!(Command::new("npx") 23 | .arg("tsc") 24 | .current_dir(dir) 25 | .output() 26 | .unwrap() 27 | .status 28 | .success()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -euo pipefail 4 | set -x 5 | 6 | # Note: cargo-wasix automatically runs wasm-opt with -O2, which makes the resulting binary unusable. 7 | # Instead, we use the toolchain to build (cargo +wasix instead of cargo wasix) and optimize manually. 8 | cargo +wasix build --target wasm32-wasmer-wasi -r 9 | mv target/wasm32-wasmer-wasi/release/winterjs.wasm x.wasm 10 | wasm-opt x.wasm -o target/wasm32-wasmer-wasi/release/winterjs.wasm -O1 --enable-bulk-memory --enable-threads --enable-reference-types --no-validation --asyncify 11 | rm x.wasm 12 | wasm-strip target/wasm32-wasmer-wasi/release/winterjs.wasm -------------------------------------------------------------------------------- /docs/References.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | Links to related resources. 4 | 5 | 6 | * Winter CG: 7 | Community group defining standards for server-side Javascript 8 | https://wintercg.org/work 9 | - [Minimum API](https://common-min-api.proposal.wintercg.org/) 10 | - [Web Crypto Streams](https://webcrypto-streams.proposal.wintercg.org/) 11 | - [Performance](https://github.com/wintercg/performance) 12 | (like `performance.now()`) 13 | - [fetch](https://fetch.spec.wintercg.org/) 14 | 15 | 16 | ## Spidermonkey 17 | 18 | * [Cookbook examples](https://github.com/mozilla-spidermonkey/spidermonkey-embedding-examples) 19 | Examples for common spidermonkey operations. 20 | -------------------------------------------------------------------------------- /examples/echo/app.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: wasmer.io/App.v0 3 | name: wasmer-winter-js-echo 4 | package: wasmer-examples/winter-js-echo 5 | -------------------------------------------------------------------------------- /examples/echo/wasmer.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'wasmer-examples/winter-js-echo' 3 | version = '0.2.0' 4 | description = 'Javascript echo server using winter-js.' 5 | 6 | # See more keys and definitions at https://docs.wasmer.io/registry/manifest 7 | 8 | [dependencies] 9 | "wasmer/winterjs" = "*" 10 | 11 | [fs] 12 | "/app" = "./app" 13 | 14 | [[command]] 15 | name = "script" 16 | module = "wasmer/winterjs:winterjs" 17 | runner = "https://webc.org/runner/wasi" 18 | 19 | [command.annotations.wasi] 20 | main-args = ["serve", "/app/app.js", "--script"] -------------------------------------------------------------------------------- /examples/hello-world.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function handler(req) { 4 | return new Response('Hello, world!', { 5 | headers: { 6 | 'content-type': 'text/plain', 7 | }, 8 | }); 9 | } 10 | 11 | addEventListener('fetch', (ev) => ev.respondWith(handler(ev.request))); 12 | -------------------------------------------------------------------------------- /examples/http-echo/README.md: -------------------------------------------------------------------------------- 1 | # http-echo-plain 2 | 3 | A simple http echo server that responds with information about the incoming 4 | request. 5 | 6 | The response format can be customized with: 7 | 8 | * The `Accept` header (`application/json` or `text/html`) 9 | * The `?format` query param 10 | - `json` 11 | - `html` 12 | - `echo` (returns request headers and body unmodified) 13 | 14 | ## Running locally 15 | 16 | ```bash 17 | wasmer run wasmer/winterjs --net --mapdir=src:src -- src/index.js --watch 18 | ``` 19 | 20 | This example is also deployed to Wasmer Edge at: https://httpinfo-winterjs.wasmer.app . 21 | -------------------------------------------------------------------------------- /examples/http-echo/app.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: wasmer.io/App.v0 3 | name: httpinfo-winterjs 4 | package: wasmer-examples/http-echo-winterjs@0.1.0 5 | debug: false 6 | app_id: da_p2qMIbt8UvX2 7 | -------------------------------------------------------------------------------- /examples/http-echo/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | async function handleRequest(req) { 3 | const accept = req.headers.get('accept') ?? ''; 4 | const url = new URL(req.url); 5 | const queryFormat = url.searchParams.get('format'); 6 | 7 | let outputFormat = 'html'; 8 | 9 | if (queryFormat) { 10 | switch (queryFormat) { 11 | case 'json': 12 | outputFormat = 'json'; 13 | break; 14 | case 'html': 15 | outputFormat = 'html'; 16 | break; 17 | case 'echo': 18 | outputFormat = 'echo'; 19 | break; 20 | } 21 | } else if (accept) { 22 | if (accept.startsWith('application/json')) { 23 | outputFormat = 'json'; 24 | } else if (accept.search('text/html') !== -1) { 25 | outputFormat = 'html'; 26 | } 27 | } 28 | 29 | switch (outputFormat) { 30 | case 'json': 31 | return buildResponseJson(req); 32 | case 'html': 33 | return buildResponseHtml(req); 34 | case 'echo': 35 | return buildResponseEcho(req); 36 | } 37 | } 38 | 39 | async function requestBodyToString(req) { 40 | try { 41 | const body = await req.text(); 42 | return !body ? '' : body; 43 | } catch (e) { 44 | console.warn("Could not decode request body", e); 45 | return ""; 46 | } 47 | } 48 | 49 | 50 | addEventListener("fetch", (ev) => { 51 | ev.respondWith(handleRequest(ev.request)); 52 | }); 53 | 54 | async function buildResponseJson(req) { 55 | const reqBody = await requestBodyToString(req); 56 | 57 | const data = { 58 | url: req.url, 59 | method: req.method, 60 | headers: Object.fromEntries(req.headers), 61 | body: reqBody, 62 | }; 63 | const body = JSON.stringify(data, null, 2); 64 | return new Response(body, { 65 | headers: { "content-type": "application/json" }, 66 | }); 67 | } 68 | 69 | async function buildResponseHtml(req) { 70 | 71 | let headers = ''; 72 | 73 | for (const [key, value] of req.headers.entries()) { 74 | headers += ` 75 | 76 | ${key} 77 | ${value} 78 | `; 79 | } 80 | 81 | const url = new URL(req.url); 82 | url.searchParams.set('format', 'json'); 83 | const jsonUrl = url.pathname + url.search; 84 | 85 | const reqBody = await requestBodyToString(req); 86 | 87 | let html = ` 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | HTTP-Info - Analyze HTTP requests 96 | 97 | 98 | 99 |
100 |
101 |

102 | HTTP-Info 103 |

104 | 105 |
106 | JSON 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | 134 | 135 | 136 |
URL${req.url}
Method${req.method}
Headers 125 | 126 | 127 | ${headers} 128 | 129 |
130 |
Body${reqBody}
137 | 138 |
139 |
140 |

Info

141 |
142 |
143 |

This service provides information about the incoming HTTP request. 144 | It is useful for debugging and analyzing HTTP clients.

145 | 146 |

You can control the output format by:

147 | 148 |
    149 |
  • Setting the Accept header to application/json or text/html
  • 150 |
  • 151 | Setting the ?format=XXX query parameter to 152 | json, html or echo
  • . 153 |
154 | 155 |

156 | By default the output format is html.
157 | If the format is echo, the response will contain 158 | the request headers and body unchanged. 159 |

160 | 161 |
162 |
163 | 164 | 172 |
173 | 174 |
175 | 176 | 177 | `; 178 | 179 | return new Response(html, { 180 | headers: { "content-type": "text/html" }, 181 | }); 182 | } 183 | 184 | function buildResponseEcho(req) { 185 | return new Response(req.body, { 186 | headers: req.headers, 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /examples/http-echo/wasmer.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmer-examples/http-echo-winterjs" 3 | version = "0.1.0" 4 | description = "A simple HTTP echo server using WinterJS - responds with request information." 5 | 6 | [dependencies] 7 | "wasmer/winterjs" = "*" 8 | 9 | [fs] 10 | "/src" = "./src" 11 | 12 | [[command]] 13 | name = "script" 14 | module = "wasmer/winterjs:winterjs" 15 | runner = "https://webc.org/runner/wasi" 16 | 17 | [command.annotations.wasi] 18 | main-args = ["serve", "/src/index.js", "--script"] 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flakeutils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1702938738, 24 | "narHash": "sha256-O7Vb0xC9s4Dmgxj8APEpuuMj7HsLgPbpy1UKvNVJp7o=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "dd8e82f3b4017b8faa52c2b1897a38d53c3c26cb", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "id": "nixpkgs", 32 | "type": "indirect" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "flakeutils": "flakeutils", 38 | "nixpkgs": "nixpkgs" 39 | } 40 | }, 41 | "systems": { 42 | "locked": { 43 | "lastModified": 1681028828, 44 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 45 | "owner": "nix-systems", 46 | "repo": "default", 47 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "nix-systems", 52 | "repo": "default", 53 | "type": "github" 54 | } 55 | } 56 | }, 57 | "root": "root", 58 | "version": 7 59 | } 60 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "winterjs - A Javascript runtime following the WinterCG spec."; 3 | 4 | inputs = { 5 | flakeutils = { 6 | url = "github:numtide/flake-utils"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | 11 | outputs = { self, nixpkgs, flakeutils }: 12 | flakeutils.lib.eachDefaultSystem (system: 13 | let 14 | NAME = "winterjs"; 15 | VERSION = "0.1.0"; 16 | 17 | pkgs = import nixpkgs { 18 | inherit system; 19 | }; 20 | 21 | llvm = pkgs.llvmPackages_latest; 22 | in 23 | rec { 24 | 25 | # packages.${NAME} = pkgs.stdenv.mkDerivation { 26 | # pname = NAME; 27 | # version = VERSION; 28 | 29 | # buildPhase = "echo 'no-build'"; 30 | # }; 31 | 32 | # defaultPackage = packages.${NAME}; 33 | 34 | # For `nix run`. 35 | # apps.${NAME} = flakeutils.lib.mkApp { 36 | # drv = packages.${NAME}; 37 | # }; 38 | # defaultApp = apps.${NAME}; 39 | 40 | devShell = pkgs.mkShell.override { stdenv = pkgs.llvmPackages_latest.stdenv; } rec { 41 | packages = with pkgs; [ 42 | # llvmPackages_16.bintools-unwrapped 43 | pkg-config 44 | gnum4 45 | 46 | # builder 47 | gnumake 48 | cmake 49 | bear 50 | 51 | # debugger 52 | llvmPackages_latest.lldb 53 | gdb 54 | 55 | # fix headers not found 56 | clang-tools 57 | 58 | # LSP and compiler 59 | llvmPackages_latest.libstdcxxClang 60 | 61 | # other tools 62 | cppcheck 63 | llvmPackages_latest.libllvm 64 | valgrind 65 | 66 | # stdlib for cpp 67 | llvmPackages_latest.libcxx 68 | 69 | # libs 70 | llvmPackages_latest.libclang 71 | zlib 72 | ]; 73 | 74 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath packages; 75 | 76 | 77 | shellHook = '' 78 | export AS="$CC -c" 79 | ''; 80 | }; 81 | 82 | # devShell = pkgs.stdenv.mkDerivation { 83 | # name = NAME; 84 | # src = self; 85 | # buildInputs = with pkgs; [ 86 | # ]; 87 | # runtimeDependencies = with pkgs; [ ]; 88 | 89 | # LD_LIBRARY_PATH= pkgs.lib.makeLibraryPath (with pkgs; [ 90 | # zlib 91 | # llvmPackages_16.libclang 92 | # ]); 93 | 94 | # # standalone as(1) doesn’t treat -DNDEBUG as -D NDEBUG (define), but rather -D (produce 95 | # # assembler debugging messages) + -N (invalid option); see also 96 | # # /nix/store/a64w6zy8w9hcj6b4g5nz0dl6zyd24c1x-gcc-wrapper-11.3.0/bin/as: invalid option -- 'N' 97 | # # make[4]: *** [/path/to/mozjs/mozjs/mozjs/config/rules.mk:664: icu_data.o] Error 1 98 | # # make[3]: *** [/path/to/mozjs/mozjs/mozjs/config/recurse.mk:72: config/external/icu/data/target-objects] Error 2 99 | # AS="$CC -c"; 100 | # }; 101 | } 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "winterjs", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "typescript": "^5.3.3" 9 | } 10 | }, 11 | "node_modules/typescript": { 12 | "version": "5.3.3", 13 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 14 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 15 | "dev": true, 16 | "bin": { 17 | "tsc": "bin/tsc", 18 | "tsserver": "bin/tsserver" 19 | }, 20 | "engines": { 21 | "node": ">=14.17" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^5.3.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.76 -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | pkgs.clangStdenv.mkDerivation { 3 | name = "mozjs-shell"; 4 | 5 | shellHook = '' 6 | export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath [ 7 | pkgs.zlib 8 | pkgs.libclang 9 | ]} 10 | 11 | # standalone as(1) doesn’t treat -DNDEBUG as -D NDEBUG (define), but rather -D (produce 12 | # assembler debugging messages) + -N (invalid option); see also 13 | # /nix/store/a64w6zy8w9hcj6b4g5nz0dl6zyd24c1x-gcc-wrapper-11.3.0/bin/as: invalid option -- 'N' 14 | # make[4]: *** [/path/to/mozjs/mozjs/mozjs/config/rules.mk:664: icu_data.o] Error 1 15 | # make[3]: *** [/path/to/mozjs/mozjs/mozjs/config/recurse.mk:72: config/external/icu/data/target-objects] Error 2 16 | export AS="$CC -c" 17 | ''; 18 | 19 | buildInputs = [ 20 | pkgs.rustup 21 | pkgs.python3 22 | pkgs.perl 23 | 24 | pkgs.llvmPackages.bintools-unwrapped 25 | pkgs.pkg-config 26 | pkgs.gnum4 27 | 28 | pkgs.zlib 29 | pkgs.libclang 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/builtins/cache/cache_storage.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use ion::{ 4 | class::Reflector, 5 | conversions::ToValue, 6 | function::Opt, 7 | string::byte::{ByteString, VerbatimBytes}, 8 | ClassDefinition, Context, Heap, Object, Promise, Result, Value, 9 | }; 10 | use lazy_static::lazy_static; 11 | use mozjs_sys::jsapi::JSObject; 12 | use runtime::globals::fetch::RequestInfo; 13 | 14 | use crate::{ion_err, ion_mk_err}; 15 | 16 | use super::{Cache, CacheQueryOptions}; 17 | 18 | lazy_static! { 19 | static ref DEFAULT_CACHE_KEY: ByteString = 20 | ByteString::from("_____WINTERJS_DEFAULT_CACHE_____".to_string().into()) 21 | .expect("Should be able to create default cache key"); 22 | } 23 | 24 | #[derive(FromValue)] 25 | pub struct MultiCacheQueryOptions { 26 | ignore_search: Option, 27 | ignore_method: Option, 28 | ignore_vary: Option, 29 | cache_name: Option>, 30 | } 31 | 32 | // (Request, Response) 33 | pub(super) type CacheEntryList = Vec<(Heap<*mut JSObject>, Heap<*mut JSObject>)>; 34 | 35 | #[js_class] 36 | pub struct CacheStorage { 37 | reflector: Reflector, 38 | 39 | // Note: The order of the caches is important, so we can't naively use a hashmap here 40 | #[trace(no_trace)] 41 | pub(super) caches: Vec<(ByteString, Rc>)>, 42 | } 43 | 44 | #[js_class] 45 | impl CacheStorage { 46 | #[ion(constructor)] 47 | pub fn constructor() -> Result { 48 | ion_err!("Cannot construct this type", Type) 49 | } 50 | 51 | #[ion(name = "match")] 52 | pub fn r#match( 53 | &self, 54 | cx: &Context, 55 | key: RequestInfo, 56 | Opt(options): Opt, 57 | ) -> Promise { 58 | let cache_name = options.as_ref().and_then(|o| o.cache_name.as_ref()); 59 | let query_options = options 60 | .as_ref() 61 | .map(|o| CacheQueryOptions { 62 | ignore_method: o.ignore_method, 63 | ignore_search: o.ignore_search, 64 | ignore_vary: o.ignore_vary, 65 | }) 66 | .unwrap_or_default(); 67 | for c in &self.caches { 68 | if let Some(cache_name) = cache_name { 69 | if &c.0 != cache_name { 70 | continue; 71 | } 72 | } 73 | 74 | let responses = match Cache::match_all_impl( 75 | c.1.try_borrow().expect("Should be able to borrow entries"), 76 | cx, 77 | Some(key.clone()), 78 | 1, 79 | &query_options, 80 | ) { 81 | Ok(r) => r, 82 | Err(e) => return Promise::rejected(cx, e), 83 | }; 84 | 85 | if !responses.is_empty() { 86 | return Promise::resolved(cx, responses[0]); 87 | } 88 | } 89 | 90 | Promise::resolved(cx, Value::undefined(cx)) 91 | } 92 | 93 | pub fn has(&self, cx: &Context, key: ByteString) -> Promise { 94 | Promise::resolved(cx, self.caches.iter().any(|c| c.0 == key)) 95 | } 96 | 97 | pub fn open(&mut self, cx: &Context, key: ByteString) -> Promise { 98 | let index = 99 | match self 100 | .caches 101 | .iter() 102 | .enumerate() 103 | .find_map(|(i, c)| if c.0 == key { Some(i) } else { None }) 104 | { 105 | Some(i) => i, 106 | None => { 107 | self.caches.push((key, Rc::new(RefCell::new(vec![])))); 108 | self.caches.len() - 1 109 | } 110 | }; 111 | 112 | let cache = Cache::new_object(cx, Box::new(Cache::new(self.caches[index].1.clone()))); 113 | Promise::resolved(cx, cache) 114 | } 115 | 116 | pub fn delete(&mut self, cx: &Context, key: ByteString) -> Promise { 117 | if key == *DEFAULT_CACHE_KEY { 118 | return Promise::rejected(cx, ion_mk_err!("Cannot delete the default cache", Type)); 119 | } 120 | 121 | let index = self 122 | .caches 123 | .iter() 124 | .enumerate() 125 | .find(|c| c.1 .0 == key) 126 | .map(|c| c.0); 127 | if let Some(index) = index { 128 | self.caches.remove(index); 129 | Promise::resolved(cx, true) 130 | } else { 131 | Promise::resolved(cx, false) 132 | } 133 | } 134 | 135 | pub fn keys(&self, cx: &Context) -> Promise { 136 | let result = self.caches.iter().map(|c| c.0.clone()).collect::>(); 137 | Promise::resolved(cx, result) 138 | } 139 | 140 | #[ion(get)] 141 | pub fn get_default(&self, cx: &Context) -> *mut JSObject { 142 | assert!(!self.caches.is_empty() && self.caches[0].0 == *DEFAULT_CACHE_KEY); 143 | Cache::new_object(cx, Box::new(Cache::new(self.caches[0].1.clone()))) 144 | } 145 | } 146 | 147 | pub fn define(cx: &Context, global: &Object) -> bool { 148 | if !CacheStorage::init_class(cx, global).0 { 149 | return false; 150 | } 151 | 152 | let mut caches = CacheStorage { 153 | reflector: Default::default(), 154 | caches: Default::default(), 155 | }; 156 | 157 | caches 158 | .caches 159 | .push((DEFAULT_CACHE_KEY.clone(), Rc::new(RefCell::new(vec![])))); 160 | 161 | let caches_obj = CacheStorage::new_object(cx, Box::new(caches)); 162 | global.set( 163 | cx, 164 | "caches", 165 | &Object::from(cx.root(caches_obj)).as_value(cx), 166 | ) 167 | } 168 | -------------------------------------------------------------------------------- /src/builtins/core/core.js: -------------------------------------------------------------------------------- 1 | export const getPromiseState = ________winterjs_core_Internal______.getPromiseState; 2 | export const setPromiseHooks = ________winterjs_core_Internal______.setPromiseHooks; 3 | 4 | export default Object.freeze(________winterjs_core_Internal______); 5 | -------------------------------------------------------------------------------- /src/builtins/core/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, os::raw::c_void}; 2 | 3 | use ion::{function_spec, Context, Function, Local, Object, PermanentHeap, Promise, Result, Value}; 4 | use mozjs::{ 5 | glue::{CreatePromiseLifecycleCallbacks, PromiseLifecycleTraps}, 6 | jsapi::{Handle, SetPromiseLifecycleCallbacks}, 7 | }; 8 | use mozjs_sys::jsapi::{JSContext, JSFunction, JSFunctionSpec, JSObject}; 9 | use runtime::module::NativeModule; 10 | 11 | use crate::ion_mk_err; 12 | 13 | thread_local! { 14 | static CALLBACKS_REGISTERED: RefCell = RefCell::new(false); 15 | static INIT: RefCell>> = RefCell::new(None); 16 | static BEFORE: RefCell>> = RefCell::new(None); 17 | static AFTER: RefCell>> = RefCell::new(None); 18 | static RESOLVE: RefCell>> = RefCell::new(None); 19 | } 20 | 21 | static TRAPS: PromiseLifecycleTraps = PromiseLifecycleTraps { 22 | onNewPromise: Some(on_new_promise), 23 | onBeforePromiseReaction: Some(on_before_promise_reaction), 24 | onAfterPromiseReaction: Some(on_after_promise_reaction), 25 | onPromiseSettled: Some(on_promise_settled), 26 | }; 27 | 28 | #[js_fn] 29 | fn get_promise_state(cx: &Context, promise: Object) -> Result { 30 | let promise = Promise::from(promise.into_local()) 31 | .ok_or_else(|| ion_mk_err!("The given object is not a promise", Type))?; 32 | Ok(promise.state(cx) as i32) 33 | } 34 | 35 | #[js_fn] 36 | fn set_promise_hooks( 37 | cx: &Context, 38 | init: Function, 39 | before: Function, 40 | after: Function, 41 | resolve: Function, 42 | ) { 43 | CALLBACKS_REGISTERED.with(|c| { 44 | if !*c.borrow() { 45 | unsafe { 46 | SetPromiseLifecycleCallbacks( 47 | cx.as_ptr(), 48 | CreatePromiseLifecycleCallbacks(&TRAPS, std::ptr::null()), 49 | ) 50 | }; 51 | *c.borrow_mut() = true; 52 | } 53 | }); 54 | 55 | INIT.set(Some(PermanentHeap::from_local(&init))); 56 | BEFORE.set(Some(PermanentHeap::from_local(&before))); 57 | AFTER.set(Some(PermanentHeap::from_local(&after))); 58 | RESOLVE.set(Some(PermanentHeap::from_local(&resolve))); 59 | } 60 | 61 | fn call_handler( 62 | handler: &'static std::thread::LocalKey>>>, 63 | cx: *mut JSContext, 64 | promise: Handle<*mut JSObject>, 65 | ) { 66 | let cx = unsafe { Context::new_unchecked(cx) }; 67 | if let Some(f) = handler.with(|f| f.borrow().as_ref().map(|f| f.root(&cx))) { 68 | let promise = Value::object(&cx, &unsafe { Local::from_marked(promise.ptr) }.into()); 69 | if let Err(e) = Function::from(f).call(&cx, &Object::null(&cx), &[promise]) { 70 | tracing::error!("Promise hook callback failed with {e:?}"); 71 | } 72 | } 73 | } 74 | 75 | unsafe extern "C" fn on_new_promise( 76 | _: *const c_void, 77 | cx: *mut JSContext, 78 | promise: Handle<*mut JSObject>, 79 | ) { 80 | call_handler(&INIT, cx, promise); 81 | } 82 | 83 | unsafe extern "C" fn on_before_promise_reaction( 84 | _: *const c_void, 85 | cx: *mut JSContext, 86 | promise: Handle<*mut JSObject>, 87 | ) { 88 | call_handler(&BEFORE, cx, promise); 89 | } 90 | 91 | unsafe extern "C" fn on_after_promise_reaction( 92 | _: *const c_void, 93 | cx: *mut JSContext, 94 | promise: Handle<*mut JSObject>, 95 | ) { 96 | call_handler(&AFTER, cx, promise); 97 | } 98 | 99 | unsafe extern "C" fn on_promise_settled( 100 | _: *const c_void, 101 | cx: *mut JSContext, 102 | promise: Handle<*mut JSObject>, 103 | ) { 104 | call_handler(&RESOLVE, cx, promise); 105 | } 106 | 107 | const METHODS: &[JSFunctionSpec] = &[ 108 | function_spec!(get_promise_state, "getPromiseState", 1), 109 | function_spec!(set_promise_hooks, "setPromiseHooks", 4), 110 | JSFunctionSpec::ZERO, 111 | ]; 112 | 113 | #[derive(Default)] 114 | pub struct CoreModule; 115 | 116 | impl NativeModule for CoreModule { 117 | const NAME: &'static str = "__winterjs_core_"; 118 | 119 | const SOURCE: &'static str = include_str!("core.js"); 120 | 121 | fn module(cx: &Context) -> Option { 122 | let ret = Object::new(cx); 123 | unsafe { ret.define_methods(cx, METHODS) }.then_some(ret) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/builtins/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | mod subtle; 2 | 3 | use ion::{ 4 | conversions::ToValue, function_spec, ClassDefinition, Context, Error, ErrorKind, Object, Result, 5 | }; 6 | use mozjs::{ 7 | jsapi::{DataView_ClassPtr, UnwrapFloat32Array, UnwrapFloat64Array}, 8 | typedarray::ArrayBufferView, 9 | }; 10 | use mozjs_sys::jsapi::{JSFunctionSpec, JSObject, JS_InstanceOf}; 11 | use rand::RngCore; 12 | 13 | #[js_fn] 14 | fn get_random_values(cx: &Context, array: ArrayBufferView) -> Result<*mut JSObject> { 15 | if array.len() > 65536 { 16 | return Err(Error::new("Quota exceeded", ErrorKind::Normal)); 17 | } 18 | unsafe { 19 | let rooted = cx.root(*array.underlying_object()); 20 | if !UnwrapFloat32Array(*array.underlying_object()).is_null() 21 | || !UnwrapFloat64Array(*array.underlying_object()).is_null() 22 | || JS_InstanceOf( 23 | cx.as_ptr(), 24 | rooted.handle().into(), 25 | DataView_ClassPtr, 26 | std::ptr::null_mut(), 27 | ) 28 | { 29 | return Err(Error::new("Bad array element type", ErrorKind::Type)); 30 | } 31 | } 32 | 33 | let mut array = array; 34 | let slice = unsafe { array.as_mut_slice() }; 35 | rand::thread_rng().fill_bytes(slice); 36 | 37 | // We have to call underlying_object because ToValue is not 38 | // implemented for ArrayBufferView 39 | Ok(unsafe { *array.underlying_object() }) 40 | } 41 | 42 | #[js_fn] 43 | fn random_uuid() -> String { 44 | let id = uuid::Uuid::new_v4(); 45 | id.to_string() 46 | } 47 | 48 | const METHODS: &[JSFunctionSpec] = &[ 49 | function_spec!(get_random_values, "getRandomValues", 1), 50 | function_spec!(random_uuid, "randomUUID", 0), 51 | JSFunctionSpec::ZERO, 52 | ]; 53 | 54 | pub fn define(cx: &Context, global: &ion::Object) -> bool { 55 | let crypto = Object::new(cx); 56 | let subtle = Object::new(cx); 57 | 58 | crypto.set(cx, "subtle", &subtle.as_value(cx)) 59 | && global.set(cx, "crypto", &ion::Value::object(cx, &crypto)) 60 | && subtle::define(cx, subtle) 61 | && subtle::crypto_key::CryptoKey::init_class(cx, global).0 62 | && subtle::crypto_key::KeyAlgorithm::init_class(cx, global).0 63 | && subtle::algorithm::hmac::HmacKeyAlgorithm::init_class(cx, global).0 64 | && unsafe { crypto.define_methods(cx, METHODS) } 65 | } 66 | -------------------------------------------------------------------------------- /src/builtins/crypto/subtle/algorithm/md5.rs: -------------------------------------------------------------------------------- 1 | use ion::{typedarray::ArrayBuffer, Context, Error, ErrorKind}; 2 | 3 | use super::CryptoAlgorithm; 4 | 5 | pub struct Md5; 6 | 7 | impl CryptoAlgorithm for Md5 { 8 | fn name(&self) -> &'static str { 9 | "MD5" 10 | } 11 | 12 | fn digest<'cx>( 13 | &self, 14 | cx: &'cx Context, 15 | _params: &ion::Object, 16 | data: Vec, 17 | ) -> ion::Result> { 18 | let data = md5::compute(data.as_slice()).0; 19 | ArrayBuffer::copy_from_bytes(cx, &data[..]) 20 | .ok_or_else(|| Error::new("Failed to allocate array", ErrorKind::Normal)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/builtins/crypto/subtle/algorithm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hmac; 2 | pub mod md5; 3 | pub mod sha; 4 | 5 | use ion::{typedarray::ArrayBuffer, Context, Object, Value}; 6 | 7 | use super::{ 8 | crypto_key::{CryptoKey, KeyFormat, KeyUsage}, 9 | HeapKeyData, 10 | }; 11 | 12 | #[allow(unused_variables)] 13 | pub trait CryptoAlgorithm { 14 | fn name(&self) -> &'static str; 15 | 16 | fn get_jwk_identifier(&self) -> ion::Result<&'static str> { 17 | Err(ion::Error::new( 18 | "Operation not supported by the specified algorithm", 19 | ion::ErrorKind::Normal, 20 | )) 21 | } 22 | 23 | fn encrypt<'cx>( 24 | &self, 25 | cx: &'cx Context, 26 | params: &Object, 27 | key: &CryptoKey, 28 | data: Vec, 29 | ) -> ion::Result> { 30 | Err(ion::Error::new( 31 | "Operation not supported by the specified algorithm", 32 | ion::ErrorKind::Normal, 33 | )) 34 | } 35 | 36 | fn decrypt<'cx>( 37 | &self, 38 | cx: &'cx Context, 39 | params: &Object, 40 | key: &CryptoKey, 41 | data: Vec, 42 | ) -> ion::Result> { 43 | Err(ion::Error::new( 44 | "Operation not supported by the specified algorithm", 45 | ion::ErrorKind::Normal, 46 | )) 47 | } 48 | 49 | fn sign<'cx>( 50 | &self, 51 | cx: &'cx Context, 52 | params: &Object, 53 | key: &CryptoKey, 54 | data: Vec, 55 | ) -> ion::Result> { 56 | Err(ion::Error::new( 57 | "Operation not supported by the specified algorithm", 58 | ion::ErrorKind::Normal, 59 | )) 60 | } 61 | 62 | fn verify( 63 | &self, 64 | cx: &Context, 65 | params: &Object, 66 | key: &CryptoKey, 67 | signature: Vec, 68 | data: Vec, 69 | ) -> ion::Result { 70 | Err(ion::Error::new( 71 | "Operation not supported by the specified algorithm", 72 | ion::ErrorKind::Normal, 73 | )) 74 | } 75 | 76 | fn digest<'cx>( 77 | &self, 78 | cx: &'cx Context, 79 | params: &Object, 80 | data: Vec, 81 | ) -> ion::Result> { 82 | Err(ion::Error::new( 83 | "Operation not supported by the specified algorithm", 84 | ion::ErrorKind::Normal, 85 | )) 86 | } 87 | 88 | fn derive_bits<'cx>( 89 | &self, 90 | cx: &'cx Context, 91 | params: &Object, 92 | base_key: CryptoKey, 93 | length: usize, 94 | ) -> ion::Result> { 95 | Err(ion::Error::new( 96 | "Operation not supported by the specified algorithm", 97 | ion::ErrorKind::Normal, 98 | )) 99 | } 100 | 101 | fn wrap_key<'cx>( 102 | &self, 103 | cx: &'cx Context, 104 | params: &Object, 105 | format: KeyFormat, 106 | key: &CryptoKey, 107 | wrapping_key: CryptoKey, 108 | ) -> ion::Result> { 109 | Err(ion::Error::new( 110 | "Operation not supported by the specified algorithm", 111 | ion::ErrorKind::Normal, 112 | )) 113 | } 114 | 115 | #[allow(clippy::too_many_arguments)] 116 | fn unwrap_key<'cx>( 117 | &self, 118 | _cx: &'cx Context, 119 | _params: &Object, 120 | _format: KeyFormat, 121 | _wrapped_key: Vec, 122 | _unwrapping_key: &CryptoKey, 123 | _extractable: bool, 124 | _usages: Vec, 125 | ) -> ion::Result> { 126 | Err(ion::Error::new( 127 | "Operation not supported by the specified algorithm", 128 | ion::ErrorKind::Normal, 129 | )) 130 | } 131 | 132 | fn generate_key( 133 | &self, 134 | cx: &Context, 135 | params: &Object, 136 | extractable: bool, 137 | usages: Vec, 138 | ) -> ion::Result { 139 | Err(ion::Error::new( 140 | "Operation not supported by the specified algorithm", 141 | ion::ErrorKind::Normal, 142 | )) 143 | } 144 | 145 | fn import_key( 146 | &self, 147 | cx: &Context, 148 | params: &Object, 149 | format: KeyFormat, 150 | key_data: HeapKeyData, 151 | extractable: bool, 152 | usages: Vec, 153 | ) -> ion::Result { 154 | Err(ion::Error::new( 155 | "Operation not supported by the specified algorithm", 156 | ion::ErrorKind::Normal, 157 | )) 158 | } 159 | 160 | fn export_key<'cx>( 161 | &self, 162 | cx: &'cx Context, 163 | format: KeyFormat, 164 | key: &CryptoKey, 165 | ) -> ion::Result> { 166 | Err(ion::Error::new( 167 | "Operation not supported by the specified algorithm", 168 | ion::ErrorKind::Normal, 169 | )) 170 | } 171 | 172 | fn get_key_length(&self, cx: &Context, params: &Object) -> ion::Result { 173 | Err(ion::Error::new( 174 | "Operation not supported by the specified algorithm", 175 | ion::ErrorKind::Normal, 176 | )) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/builtins/crypto/subtle/algorithm/sha.rs: -------------------------------------------------------------------------------- 1 | use ion::{typedarray::ArrayBuffer, Context, Error, ErrorKind}; 2 | use sha2::Digest; 3 | 4 | use super::CryptoAlgorithm; 5 | 6 | pub enum Sha { 7 | Sha1, 8 | Sha256, 9 | Sha384, 10 | Sha512, 11 | } 12 | 13 | impl CryptoAlgorithm for Sha { 14 | fn name(&self) -> &'static str { 15 | match self { 16 | Self::Sha1 => "SHA-1", 17 | Self::Sha256 => "SHA-256", 18 | Self::Sha384 => "SHA-384", 19 | Self::Sha512 => "SHA-512", 20 | } 21 | } 22 | 23 | fn get_jwk_identifier(&self) -> ion::Result<&'static str> { 24 | Ok(match self { 25 | Self::Sha1 => "HS1", 26 | Self::Sha256 => "HS256", 27 | Self::Sha384 => "HS384", 28 | Self::Sha512 => "HS512", 29 | }) 30 | } 31 | 32 | fn digest<'cx>( 33 | &self, 34 | cx: &'cx Context, 35 | _params: &ion::Object, 36 | data: Vec, 37 | ) -> ion::Result> { 38 | match self { 39 | Self::Sha1 => { 40 | let data = sha1::Sha1::digest(data.as_slice()); 41 | ArrayBuffer::copy_from_bytes(cx, &data) 42 | .ok_or_else(|| Error::new("Failed to allocate array", ErrorKind::Normal)) 43 | } 44 | 45 | Self::Sha256 => { 46 | let data = sha2::Sha256::digest(data.as_slice()); 47 | ArrayBuffer::copy_from_bytes(cx, &data) 48 | .ok_or_else(|| Error::new("Failed to allocate array", ErrorKind::Normal)) 49 | } 50 | 51 | Self::Sha384 => { 52 | let data = sha2::Sha384::digest(data.as_slice()); 53 | ArrayBuffer::copy_from_bytes(cx, &data) 54 | .ok_or_else(|| Error::new("Failed to allocate array", ErrorKind::Normal)) 55 | } 56 | 57 | Self::Sha512 => { 58 | let data = sha2::Sha512::digest(data.as_slice()); 59 | ArrayBuffer::copy_from_bytes(cx, &data) 60 | .ok_or_else(|| Error::new("Failed to allocate array", ErrorKind::Normal)) 61 | } 62 | } 63 | } 64 | 65 | fn get_key_length(&self, _cx: &Context, _params: &ion::Object) -> ion::Result { 66 | match self { 67 | Self::Sha1 => Ok(20), 68 | Self::Sha256 => Ok(64), 69 | Self::Sha384 => Ok(48), 70 | Self::Sha512 => Ok(512), 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/builtins/crypto/subtle/crypto_key.rs: -------------------------------------------------------------------------------- 1 | use ion::{ 2 | class::Reflector, 3 | conversions::{FromValue, ToValue}, 4 | Context, Error, ErrorKind, Heap, Result, Value, 5 | }; 6 | use mozjs_sys::jsapi::JSObject; 7 | use rand_core::CryptoRngCore; 8 | use strum::{AsRefStr, EnumString}; 9 | 10 | use crate::{enum_value, ion_err}; 11 | 12 | #[derive(EnumString, AsRefStr, Clone, Copy)] 13 | #[strum(serialize_all = "camelCase")] 14 | pub enum KeyType { 15 | Public, 16 | Private, 17 | Secret, 18 | } 19 | 20 | enum_value!(KeyType); 21 | 22 | #[derive(EnumString, AsRefStr, Clone, Copy)] 23 | #[strum(serialize_all = "camelCase")] 24 | pub enum KeyUsage { 25 | Encrypt, 26 | Decrypt, 27 | Sign, 28 | Verify, 29 | DeriveKey, 30 | DeriveBits, 31 | WrapKey, 32 | UnwrapKey, 33 | } 34 | 35 | enum_value!(KeyUsage); 36 | 37 | #[derive(EnumString, AsRefStr, Clone, Copy)] 38 | #[strum(serialize_all = "lowercase")] 39 | pub enum KeyFormat { 40 | Raw, 41 | Spki, 42 | Pkcs8, 43 | Jwk, 44 | } 45 | 46 | enum_value!(KeyFormat); 47 | 48 | #[js_class] 49 | pub struct CryptoKey { 50 | pub reflector: Reflector, 51 | pub extractable: bool, 52 | pub algorithm: Heap<*mut JSObject>, // KeyAlgorithm 53 | 54 | #[trace(no_trace)] 55 | pub key_type: KeyType, 56 | 57 | #[trace(no_trace)] 58 | pub usages: Vec, 59 | } 60 | 61 | impl CryptoKey { 62 | pub fn new( 63 | extractable: bool, 64 | algorithm: Heap<*mut JSObject>, 65 | key_type: KeyType, 66 | usages: Vec, 67 | ) -> Self { 68 | Self { 69 | reflector: Default::default(), 70 | extractable, 71 | algorithm, 72 | key_type, 73 | usages, 74 | } 75 | } 76 | 77 | pub fn set_extractable(&mut self, extractable: bool) { 78 | self.extractable = extractable; 79 | } 80 | 81 | pub fn usages(&self) -> &Vec { 82 | &self.usages 83 | } 84 | 85 | pub fn set_usages(&mut self, usages: Vec) { 86 | self.usages = usages; 87 | } 88 | } 89 | 90 | #[js_class] 91 | impl CryptoKey { 92 | #[ion(constructor)] 93 | pub fn constructor() -> Result { 94 | Err(Error::new("Cannot construct this type", ErrorKind::Type)) 95 | } 96 | 97 | #[ion(get)] 98 | pub fn get_type(&self) -> KeyType { 99 | self.key_type 100 | } 101 | 102 | #[ion(get)] 103 | pub fn get_extractable(&self) -> bool { 104 | self.extractable 105 | } 106 | 107 | #[ion(get)] 108 | pub fn get_algorithm(&self) -> *mut JSObject { 109 | self.algorithm.get() 110 | } 111 | 112 | #[ion(get)] 113 | pub fn get_usages<'cx>(&self, cx: &'cx Context) -> Value<'cx> { 114 | let mut val = Value::undefined(cx); 115 | self.usages.to_value(cx, &mut val); 116 | val 117 | } 118 | } 119 | 120 | #[js_class] 121 | pub struct KeyAlgorithm { 122 | pub reflector: Reflector, 123 | 124 | pub name: &'static str, 125 | } 126 | 127 | #[js_class] 128 | impl KeyAlgorithm { 129 | #[ion(constructor)] 130 | pub fn constructor() -> Result { 131 | ion_err!("This type cannot be constructed", Type); 132 | } 133 | 134 | #[ion(get)] 135 | pub fn get_name(&self) -> &'static str { 136 | self.name 137 | } 138 | } 139 | 140 | pub(crate) fn generate_random_key(length: usize, rng: &mut dyn CryptoRngCore) -> Vec { 141 | let mut key = vec![0u8; length]; 142 | rng.fill_bytes(key.as_mut()); 143 | key 144 | } 145 | -------------------------------------------------------------------------------- /src/builtins/crypto/subtle/jwk.rs: -------------------------------------------------------------------------------- 1 | use ion::conversions::FromValue; 2 | 3 | #[derive(FromValue, ToValue)] 4 | pub struct RsaOtherPrimesInfo { 5 | pub r: String, 6 | pub d: String, 7 | pub t: String, 8 | } 9 | 10 | #[derive(FromValue, ToValue, Default)] 11 | pub struct JsonWebKey { 12 | pub kty: String, 13 | #[ion(name = "use")] 14 | pub r#use: Option, 15 | pub key_ops: Option>, 16 | pub alg: Option, 17 | 18 | pub ext: Option, 19 | 20 | pub crv: Option, 21 | pub x: Option, 22 | pub y: Option, 23 | pub d: Option, 24 | pub n: Option, 25 | pub e: Option, 26 | pub p: Option, 27 | pub q: Option, 28 | pub dp: Option, 29 | pub dq: Option, 30 | pub qi: Option, 31 | pub oth: Option>, 32 | pub k: Option, 33 | } 34 | 35 | impl<'cx> FromValue<'cx> for Box { 36 | type Config = (); 37 | 38 | fn from_value( 39 | cx: &'cx ion::Context, 40 | value: &ion::Value, 41 | strict: bool, 42 | config: Self::Config, 43 | ) -> ion::Result { 44 | let jwk = JsonWebKey::from_value(cx, value, strict, config)?; 45 | Ok(Box::new(jwk)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/builtins/internal_js_modules.rs: -------------------------------------------------------------------------------- 1 | //! Here, we initialize those modules that are defined in JS, as 2 | //! opposed to native modules defined in Rust. The sources for 3 | //! these modules should be put under the internal_js_modules dir 4 | //! that lives next to this source file. 5 | //! 6 | //! The name of the modules will be equivalent to their path on disk, 7 | //! but forward slashes (/) will be changed to a colon (:) and the .js 8 | //! extension will be stripped. So, to create a node:buffer module, 9 | //! one must put the source under `internal_js_modules/node/buffer.js`. 10 | //! 11 | //! Note: Files that don't have a .js extension will be ignored. 12 | 13 | use anyhow::{anyhow, Context as _}; 14 | use clap::builder::OsStr; 15 | use include_dir::{include_dir, Dir, File}; 16 | use ion::{ 17 | module::{Module, ModuleRequest}, 18 | Context, 19 | }; 20 | 21 | const MODULES_DIR: Dir = include_dir!("src/builtins/internal_js_modules"); 22 | 23 | pub(super) fn define(cx: &Context) -> bool { 24 | match scan_dir(cx, &MODULES_DIR) { 25 | Ok(()) => true, 26 | Err(e) => { 27 | tracing::error!( 28 | error = %e, 29 | "Failed to load internal modules" 30 | ); 31 | false 32 | } 33 | } 34 | } 35 | 36 | fn scan_dir(cx: &Context, dir: &Dir) -> anyhow::Result<()> { 37 | for file in dir.files() { 38 | if file.path().extension() == Some(&OsStr::from("js")) { 39 | compile_and_register(cx, file)?; 40 | } 41 | } 42 | 43 | for dir in dir.dirs() { 44 | scan_dir(cx, dir)?; 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | fn compile_and_register(cx: &Context, script_file: &File) -> anyhow::Result<()> { 51 | tracing::debug!("Registering internal module at {:?}", script_file.path()); 52 | 53 | let module_name = script_file 54 | .path() 55 | .to_str() 56 | .context("Failed to convert module path to string")? 57 | .strip_suffix(".js") 58 | .context("Script file path must have a .js suffix")? 59 | .replace('/', ":"); 60 | 61 | let contents = script_file 62 | .contents_utf8() 63 | .context("Failed to convert file contents to UTF-8")?; 64 | 65 | let module = Module::compile(cx, &module_name, None, contents) 66 | .map_err(|e| anyhow::anyhow!("Module compilation failed: {e:?}"))?; 67 | 68 | match unsafe { &mut (*cx.get_inner_data().as_ptr()).module_loader } { 69 | Some(loader) => { 70 | let request = ModuleRequest::new(cx, module_name); 71 | loader 72 | .register(cx, module.module_object(), &request) 73 | .map_err(|e| anyhow!("Failed to register internal module due to: {e}"))?; 74 | Ok(()) 75 | } 76 | None => anyhow::bail!("No module loader present, cannot register internal module"), 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/builtins/internal_js_modules/__types/__winterjs_core_.d.ts: -------------------------------------------------------------------------------- 1 | type PromiseHook = (promise: Promise) => void; 2 | 3 | declare module "__winterjs_core_" { 4 | function getPromiseState(promise: Promise): number; 5 | 6 | function setPromiseHooks( 7 | init: PromiseHook, 8 | before: PromiseHook, 9 | after: PromiseHook, 10 | resolve: PromiseHook 11 | ): void; 12 | } -------------------------------------------------------------------------------- /src/builtins/internal_js_modules/__winterjs_internal/base64-js.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var lookup = [] 4 | var revLookup = [] 5 | var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array 6 | 7 | var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 8 | for (var i = 0, len = code.length; i < len; ++i) { 9 | lookup[i] = code[i] 10 | revLookup[code.charCodeAt(i)] = i 11 | } 12 | 13 | // Support decoding URL-safe base64 strings, as Node.js does. 14 | // See: https://en.wikipedia.org/wiki/Base64#URL_applications 15 | revLookup['-'.charCodeAt(0)] = 62 16 | revLookup['_'.charCodeAt(0)] = 63 17 | 18 | function getLens(b64) { 19 | var len = b64.length 20 | 21 | if (len % 4 > 0) { 22 | throw new Error('Invalid string. Length must be a multiple of 4') 23 | } 24 | 25 | // Trim off extra bytes after placeholder bytes are found 26 | // See: https://github.com/beatgammit/base64-js/issues/42 27 | var validLen = b64.indexOf('=') 28 | if (validLen === -1) validLen = len 29 | 30 | var placeHoldersLen = validLen === len 31 | ? 0 32 | : 4 - (validLen % 4) 33 | 34 | return [validLen, placeHoldersLen] 35 | } 36 | 37 | // base64 is 4/3 + up to two characters of the original data 38 | function byteLength(b64) { 39 | var lens = getLens(b64) 40 | var validLen = lens[0] 41 | var placeHoldersLen = lens[1] 42 | return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen 43 | } 44 | 45 | function _byteLength(b64, validLen, placeHoldersLen) { 46 | return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen 47 | } 48 | 49 | function toByteArray(b64) { 50 | var tmp 51 | var lens = getLens(b64) 52 | var validLen = lens[0] 53 | var placeHoldersLen = lens[1] 54 | 55 | var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) 56 | 57 | var curByte = 0 58 | 59 | // if there are placeholders, only get up to the last complete 4 chars 60 | var len = placeHoldersLen > 0 61 | ? validLen - 4 62 | : validLen 63 | 64 | var i 65 | for (i = 0; i < len; i += 4) { 66 | tmp = 67 | (revLookup[b64.charCodeAt(i)] << 18) | 68 | (revLookup[b64.charCodeAt(i + 1)] << 12) | 69 | (revLookup[b64.charCodeAt(i + 2)] << 6) | 70 | revLookup[b64.charCodeAt(i + 3)] 71 | arr[curByte++] = (tmp >> 16) & 0xFF 72 | arr[curByte++] = (tmp >> 8) & 0xFF 73 | arr[curByte++] = tmp & 0xFF 74 | } 75 | 76 | if (placeHoldersLen === 2) { 77 | tmp = 78 | (revLookup[b64.charCodeAt(i)] << 2) | 79 | (revLookup[b64.charCodeAt(i + 1)] >> 4) 80 | arr[curByte++] = tmp & 0xFF 81 | } 82 | 83 | if (placeHoldersLen === 1) { 84 | tmp = 85 | (revLookup[b64.charCodeAt(i)] << 10) | 86 | (revLookup[b64.charCodeAt(i + 1)] << 4) | 87 | (revLookup[b64.charCodeAt(i + 2)] >> 2) 88 | arr[curByte++] = (tmp >> 8) & 0xFF 89 | arr[curByte++] = tmp & 0xFF 90 | } 91 | 92 | return arr 93 | } 94 | 95 | function tripletToBase64(num) { 96 | return lookup[num >> 18 & 0x3F] + 97 | lookup[num >> 12 & 0x3F] + 98 | lookup[num >> 6 & 0x3F] + 99 | lookup[num & 0x3F] 100 | } 101 | 102 | function encodeChunk(uint8, start, end) { 103 | var tmp 104 | var output = [] 105 | for (var i = start; i < end; i += 3) { 106 | tmp = 107 | ((uint8[i] << 16) & 0xFF0000) + 108 | ((uint8[i + 1] << 8) & 0xFF00) + 109 | (uint8[i + 2] & 0xFF) 110 | output.push(tripletToBase64(tmp)) 111 | } 112 | return output.join('') 113 | } 114 | 115 | function fromByteArray(uint8) { 116 | var tmp 117 | var len = uint8.length 118 | var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes 119 | var parts = [] 120 | var maxChunkLength = 16383 // must be multiple of 3 121 | 122 | // go through the array every three bytes, we'll deal with trailing stuff later 123 | for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { 124 | parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) 125 | } 126 | 127 | // pad the end with zeros, but make sure to not forget the extra bytes 128 | if (extraBytes === 1) { 129 | tmp = uint8[len - 1] 130 | parts.push( 131 | lookup[tmp >> 2] + 132 | lookup[(tmp << 4) & 0x3F] + 133 | '==' 134 | ) 135 | } else if (extraBytes === 2) { 136 | tmp = (uint8[len - 2] << 8) + uint8[len - 1] 137 | parts.push( 138 | lookup[tmp >> 10] + 139 | lookup[(tmp >> 4) & 0x3F] + 140 | lookup[(tmp << 2) & 0x3F] + 141 | '=' 142 | ) 143 | } 144 | 145 | return parts.join('') 146 | } 147 | 148 | export { 149 | byteLength, 150 | toByteArray, 151 | fromByteArray, 152 | }; -------------------------------------------------------------------------------- /src/builtins/internal_js_modules/__winterjs_internal/ieee754.js: -------------------------------------------------------------------------------- 1 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 2 | 3 | const read = function (buffer, offset, isLE, mLen, nBytes) { 4 | let e, m 5 | const eLen = (nBytes * 8) - mLen - 1 6 | const eMax = (1 << eLen) - 1 7 | const eBias = eMax >> 1 8 | let nBits = -7 9 | let i = isLE ? (nBytes - 1) : 0 10 | const d = isLE ? -1 : 1 11 | let s = buffer[offset + i] 12 | 13 | i += d 14 | 15 | e = s & ((1 << (-nBits)) - 1) 16 | s >>= (-nBits) 17 | nBits += eLen 18 | while (nBits > 0) { 19 | e = (e * 256) + buffer[offset + i] 20 | i += d 21 | nBits -= 8 22 | } 23 | 24 | m = e & ((1 << (-nBits)) - 1) 25 | e >>= (-nBits) 26 | nBits += mLen 27 | while (nBits > 0) { 28 | m = (m * 256) + buffer[offset + i] 29 | i += d 30 | nBits -= 8 31 | } 32 | 33 | if (e === 0) { 34 | e = 1 - eBias 35 | } else if (e === eMax) { 36 | return m ? NaN : ((s ? -1 : 1) * Infinity) 37 | } else { 38 | m = m + Math.pow(2, mLen) 39 | e = e - eBias 40 | } 41 | return (s ? -1 : 1) * m * Math.pow(2, e - mLen) 42 | } 43 | 44 | const write = function (buffer, value, offset, isLE, mLen, nBytes) { 45 | let e, m, c 46 | let eLen = (nBytes * 8) - mLen - 1 47 | const eMax = (1 << eLen) - 1 48 | const eBias = eMax >> 1 49 | const rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) 50 | let i = isLE ? 0 : (nBytes - 1) 51 | const d = isLE ? 1 : -1 52 | const s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 53 | 54 | value = Math.abs(value) 55 | 56 | if (isNaN(value) || value === Infinity) { 57 | m = isNaN(value) ? 1 : 0 58 | e = eMax 59 | } else { 60 | e = Math.floor(Math.log(value) / Math.LN2) 61 | if (value * (c = Math.pow(2, -e)) < 1) { 62 | e-- 63 | c *= 2 64 | } 65 | if (e + eBias >= 1) { 66 | value += rt / c 67 | } else { 68 | value += rt * Math.pow(2, 1 - eBias) 69 | } 70 | if (value * c >= 2) { 71 | e++ 72 | c /= 2 73 | } 74 | 75 | if (e + eBias >= eMax) { 76 | m = 0 77 | e = eMax 78 | } else if (e + eBias >= 1) { 79 | m = ((value * c) - 1) * Math.pow(2, mLen) 80 | e = e + eBias 81 | } else { 82 | m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) 83 | e = 0 84 | } 85 | } 86 | 87 | while (mLen >= 8) { 88 | buffer[offset + i] = m & 0xff 89 | i += d 90 | m /= 256 91 | mLen -= 8 92 | } 93 | 94 | e = (e << mLen) | m 95 | eLen += mLen 96 | while (eLen > 0) { 97 | buffer[offset + i] = e & 0xff 98 | i += d 99 | e /= 256 100 | eLen -= 8 101 | } 102 | 103 | buffer[offset + i - d] |= s * 128 104 | } 105 | 106 | export { 107 | read, 108 | write, 109 | } -------------------------------------------------------------------------------- /src/builtins/internal_js_modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2022", "DOM"], 4 | "module": "ES2022", 5 | "target": "ES2022" 6 | }, 7 | } -------------------------------------------------------------------------------- /src/builtins/js_globals.rs: -------------------------------------------------------------------------------- 1 | //! Here, we initialize globals that are defined in JS, as opposed 2 | //! to defining classes in Rust. The scripts need to do something 3 | //! meaningful, such as making assignments to `globalThis`. 4 | //! 5 | //! Note: Files that don't have a .js extension will be ignored. 6 | 7 | use anyhow::{bail, Context as _}; 8 | use clap::builder::OsStr; 9 | use include_dir::{include_dir, Dir, File}; 10 | use ion::{script::Script, Context}; 11 | 12 | const MODULES_DIR: Dir = include_dir!("src/builtins/js_globals"); 13 | 14 | pub(super) fn define(cx: &Context) -> bool { 15 | match scan_dir(cx, &MODULES_DIR) { 16 | Ok(()) => true, 17 | Err(e) => { 18 | tracing::error!( 19 | error = %e, 20 | "Failed to load internal modules" 21 | ); 22 | false 23 | } 24 | } 25 | } 26 | 27 | fn scan_dir(cx: &Context, dir: &Dir) -> anyhow::Result<()> { 28 | for file in dir.files() { 29 | if file.path().extension() == Some(&OsStr::from("js")) { 30 | compile_and_evaluate(cx, file)?; 31 | } 32 | } 33 | 34 | for dir in dir.dirs() { 35 | scan_dir(cx, dir)?; 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | fn compile_and_evaluate(cx: &Context, script_file: &File) -> anyhow::Result<()> { 42 | tracing::debug!("Evaluating global script at {:?}", script_file.path()); 43 | 44 | let contents = script_file 45 | .contents_utf8() 46 | .context("Failed to convert file contents to UTF-8")?; 47 | 48 | let script = Script::compile(cx, script_file.path(), contents) 49 | .map_err(|e| anyhow::anyhow!("Script compilation failed: {e:?}"))?; 50 | 51 | match script.evaluate(cx) { 52 | Ok(val) => { 53 | if !val.get().is_undefined() { 54 | tracing::warn!( 55 | "js_globals script {} returned a result, ignoring", 56 | script_file.path().to_string_lossy() 57 | ); 58 | } 59 | Ok(()) 60 | } 61 | Err(e) => bail!("Script execution failed: {e:?}"), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/builtins/js_globals/readable-stream.js: -------------------------------------------------------------------------------- 1 | // The definition of ReadableStream is in SpiderMonkey's C++ source, and 2 | // I'd rather not mess with that just to add in a couple of short functions. 3 | 4 | (function () { 5 | class ReadableStreamAsyncIterator { 6 | reader; 7 | 8 | constructor(stream) { 9 | this.reader = stream.getReader(); 10 | } 11 | 12 | next() { 13 | return this.reader.read(); 14 | } 15 | 16 | [Symbol.asyncIterator]() { 17 | return this; 18 | } 19 | } 20 | 21 | globalThis.ReadableStream.prototype[Symbol.asyncIterator] = function () { 22 | return new ReadableStreamAsyncIterator(this); 23 | }; 24 | 25 | globalThis.ReadableStream.prototype["values"] = function () { 26 | return new ReadableStreamAsyncIterator(this); 27 | }; 28 | 29 | globalThis.ReadableStream.from = function (iterable) { 30 | if (iterable[Symbol.iterator]) { 31 | const iterator = iterable[Symbol.iterator](); 32 | return new ReadableStream({ 33 | pull: function (controller) { 34 | const next = iterator.next(); 35 | if (next.done) { 36 | controller.close(); 37 | } else { 38 | if (typeof next.value === "undefined") { 39 | throw new TypeError( 40 | "Iterator passed to ReadableStream.from failed to produce an element" 41 | ); 42 | } 43 | controller.enqueue(next.value); 44 | } 45 | }, 46 | }); 47 | } else if (iterable[Symbol.asyncIterator]) { 48 | const iterator = iterable[Symbol.asyncIterator](); 49 | return new ReadableStream({ 50 | pull: async function (controller) { 51 | const next = await iterator.next(); 52 | if (next.done) { 53 | controller.close(); 54 | } else { 55 | if (typeof next.value === "undefined") { 56 | throw new TypeError( 57 | "Iterator passed to ReadableStream.from failed to produce an element" 58 | ); 59 | } 60 | controller.enqueue(next.value); 61 | } 62 | }, 63 | }); 64 | } else { 65 | throw new TypeError( 66 | "The parameter passed to ReadableStream.from is not iterable" 67 | ); 68 | } 69 | }; 70 | })(); 71 | -------------------------------------------------------------------------------- /src/builtins/js_globals/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2022", "DOM"], 4 | "module": "ES2022", 5 | "target": "ES2022" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/builtins/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::Context; 2 | use runtime::module::{init_global_module, init_module, StandardModules}; 3 | 4 | pub mod cache; 5 | pub mod core; 6 | pub mod crypto; 7 | pub mod internal_js_modules; 8 | pub mod js_globals; 9 | pub mod navigator; 10 | pub mod performance; 11 | pub mod process; 12 | 13 | pub struct Modules { 14 | pub include_internal: bool, 15 | pub hardware_concurrency: u32, 16 | } 17 | 18 | impl Modules { 19 | fn define_common(&self, cx: &Context, global: &ion::Object) -> bool { 20 | init_global_module::(cx, global) 21 | && init_global_module::(cx, global) 22 | && init_global_module::(cx, global) 23 | && init_global_module::(cx, global) 24 | && performance::define(cx, global) 25 | && process::define(cx, global) 26 | && crypto::define(cx, global) 27 | && cache::define(cx, global) 28 | && navigator::define(cx, global, self.hardware_concurrency) 29 | } 30 | } 31 | 32 | impl StandardModules for Modules { 33 | fn init(self, cx: &Context, global: &ion::Object) -> bool { 34 | let result = init_module::(cx, global) 35 | && self.define_common(cx, global) 36 | && js_globals::define(cx); 37 | 38 | if self.include_internal { 39 | result && internal_js_modules::define(cx) 40 | } else { 41 | result 42 | } 43 | } 44 | 45 | fn init_globals(self, cx: &Context, global: &ion::Object) -> bool { 46 | if self.include_internal { 47 | tracing::error!( 48 | "Internal error: trying to initialize internal modules in global object mode" 49 | ); 50 | return false; 51 | } 52 | 53 | self.define_common(cx, global) && js_globals::define(cx) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/builtins/navigator.rs: -------------------------------------------------------------------------------- 1 | use ion::{class::Reflector, flags::PropertyFlags, ClassDefinition, Context, Result, Value}; 2 | 3 | use crate::ion_err; 4 | 5 | #[js_class] 6 | pub struct Navigator { 7 | reflector: Reflector, 8 | 9 | concurrency: u32, 10 | language: String, 11 | } 12 | 13 | #[js_class] 14 | impl Navigator { 15 | #[ion(constructor)] 16 | pub fn constructor() -> Result { 17 | ion_err!("Cannot construct this type", Type) 18 | } 19 | 20 | #[ion(get, name = "userAgent")] 21 | pub fn get_user_agent(&self) -> &'static str { 22 | "WinterJS" 23 | } 24 | 25 | #[ion(get)] 26 | pub fn get_platform(&self) -> &'static str { 27 | "WASIX-Wasm32" 28 | } 29 | 30 | #[ion(get, name = "hardwareConcurrency")] 31 | pub fn get_hardware_concurrency(&self) -> u32 { 32 | self.concurrency 33 | } 34 | 35 | #[ion(get)] 36 | pub fn get_language(&self) -> &str { 37 | &self.language 38 | } 39 | 40 | #[ion(get)] 41 | pub fn get_languages(&self) -> Vec<&str> { 42 | vec![&self.language] 43 | } 44 | } 45 | 46 | pub fn define(cx: &Context, global: &ion::Object, concurrency: u32) -> bool { 47 | if !Navigator::init_class(cx, global).0 { 48 | return false; 49 | } 50 | 51 | let navigator = Navigator::new_rooted( 52 | cx, 53 | Box::new(Navigator { 54 | reflector: Default::default(), 55 | concurrency, 56 | language: sys_locale::get_locale().unwrap_or("en-US".into()), 57 | }), 58 | ); 59 | 60 | global.define( 61 | cx, 62 | "navigator", 63 | &Value::object(cx, &navigator), 64 | PropertyFlags::ENUMERATE, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/builtins/performance.rs: -------------------------------------------------------------------------------- 1 | use ion::{function_spec, Context, Object, Value}; 2 | use mozjs_sys::jsapi::JSFunctionSpec; 3 | 4 | lazy_static::lazy_static! { 5 | static ref PERFORMANCE_ORIGIN: std::time::Instant = std::time::Instant::now(); 6 | } 7 | 8 | #[js_fn] 9 | fn now() -> f64 { 10 | PERFORMANCE_ORIGIN.elapsed().as_secs_f64() * 1_000.0 11 | } 12 | 13 | const METHODS: &[JSFunctionSpec] = &[function_spec!(now, 0), JSFunctionSpec::ZERO]; 14 | 15 | pub fn define(cx: &Context, global: &Object) -> bool { 16 | let performance = Object::new(cx); 17 | performance.set_as(cx, "timeOrigin", &0.0f64) 18 | && unsafe { performance.define_methods(cx, METHODS) } 19 | && global.set_as(cx, "performance", &Value::object(cx, &performance)) 20 | } 21 | -------------------------------------------------------------------------------- /src/builtins/process.rs: -------------------------------------------------------------------------------- 1 | use ion::{conversions::ToValue, flags::PropertyFlags, Context, Object}; 2 | 3 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 4 | 5 | pub fn populate_env_object(cx: &Context, env: &Object) -> bool { 6 | for (name, value) in std::env::vars() { 7 | // WINTERJS_* env vars are used to pass args to WinterJS itself, and are 8 | // useless for JS code 9 | if name.starts_with("WINTERJS_") { 10 | continue; 11 | } 12 | 13 | if !env.define( 14 | cx, 15 | name.as_str(), 16 | &value.as_value(cx), 17 | PropertyFlags::ENUMERATE, 18 | ) { 19 | return false; 20 | } 21 | } 22 | 23 | true 24 | } 25 | 26 | pub fn define(cx: &Context, global: &Object) -> bool { 27 | let process = Object::new(cx); 28 | let env = Object::new(cx); 29 | populate_env_object(cx, &env); 30 | 31 | process.define(cx, "env", &env.as_value(cx), PropertyFlags::ENUMERATE) 32 | && process.define( 33 | cx, 34 | "version", 35 | &format!("WinterJS {VERSION}").as_value(cx), 36 | PropertyFlags::CONSTANT_ENUMERATED, 37 | ) 38 | && global.define( 39 | cx, 40 | "process", 41 | &process.as_value(cx), 42 | PropertyFlags::ENUMERATE, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/request_handlers/cloudflare/context.rs: -------------------------------------------------------------------------------- 1 | use ion::{ 2 | class::{ClassDefinition, Reflector}, 3 | Object, Result, 4 | }; 5 | use mozjs_sys::jsapi::JSObject; 6 | 7 | use crate::ion_err; 8 | 9 | #[js_class] 10 | pub struct Context { 11 | reflector: Reflector, 12 | } 13 | 14 | impl Context { 15 | pub fn new_obj(cx: &ion::Context) -> *mut JSObject { 16 | Self::new_object( 17 | cx, 18 | Box::new(Self { 19 | reflector: Default::default(), 20 | }), 21 | ) 22 | } 23 | } 24 | 25 | #[js_class] 26 | impl Context { 27 | #[ion(constructor)] 28 | pub fn constructor() -> Result { 29 | ion_err!("Cannot construct this type", Type) 30 | } 31 | 32 | #[ion(name = "waitUntil")] 33 | pub fn wait_until(&self, _promise: ion::Promise) { 34 | // No need to do anything, the runtime will run the promise anyway 35 | } 36 | 37 | #[ion(name = "passThroughOnException")] 38 | pub fn pass_through_on_exception() -> Result<()> { 39 | ion_err!("Not supported", Type); 40 | } 41 | } 42 | 43 | pub fn define(cx: &ion::Context, global: &Object) -> bool { 44 | Context::init_class(cx, global).0 45 | } 46 | -------------------------------------------------------------------------------- /src/request_handlers/cloudflare/env.rs: -------------------------------------------------------------------------------- 1 | use ion::{ 2 | class::{NativeObject, Reflector}, 3 | ClassDefinition, Context, Exception, Heap, Object, Promise, Result, TracedHeap, 4 | }; 5 | use mozjs_sys::jsapi::JSObject; 6 | use runtime::{ 7 | globals::fetch::Request as FetchRequest, globals::fetch::Response as FetchResponse, 8 | promise::future_to_promise, 9 | }; 10 | 11 | use crate::{ion_err, ion_mk_err}; 12 | 13 | #[js_class] 14 | pub struct Env { 15 | reflector: Reflector, 16 | assets: Heap<*mut JSObject>, 17 | } 18 | 19 | impl Env { 20 | pub fn new_obj(cx: &Context) -> *mut JSObject { 21 | let assets = EnvAssets::new_object( 22 | cx, 23 | Box::new(EnvAssets { 24 | reflector: Default::default(), 25 | }), 26 | ); 27 | let env = Object::from(cx.root(Env::new_object( 28 | cx, 29 | Box::new(Env { 30 | reflector: Default::default(), 31 | assets: Heap::new(assets), 32 | }), 33 | ))); 34 | 35 | if !crate::builtins::process::populate_env_object(cx, &env) { 36 | panic!("Failed to populate env object"); 37 | } 38 | 39 | (*env).get() 40 | } 41 | } 42 | 43 | #[js_class] 44 | impl Env { 45 | #[ion(constructor)] 46 | pub fn constructor() -> Result { 47 | ion_err!("Cannot construct this type", Type) 48 | } 49 | 50 | #[allow(non_snake_case)] 51 | #[ion(name = "ASSETS", get)] 52 | pub fn get_assets(&self) -> *mut JSObject { 53 | self.assets.get() 54 | } 55 | } 56 | 57 | #[derive(FromValue)] 58 | pub enum FetchInput<'cx> { 59 | #[ion(inherit)] 60 | Request(&'cx FetchRequest), 61 | #[ion(inherit)] 62 | Url(&'cx runtime::globals::url::URL), 63 | #[ion(inherit)] 64 | String(String), 65 | } 66 | 67 | enum FetchInputHeap { 68 | Request(TracedHeap<*mut JSObject>), 69 | Url(String), 70 | } 71 | 72 | #[js_class] 73 | pub struct EnvAssets { 74 | reflector: Reflector, 75 | } 76 | 77 | #[js_class] 78 | impl EnvAssets { 79 | #[ion(constructor)] 80 | pub fn constructor() -> Result { 81 | ion_err!("Cannot construct this type", Type) 82 | } 83 | 84 | pub fn fetch<'cx>(&self, cx: &'cx Context, input: FetchInput<'cx>) -> Option { 85 | let input_heap = match input { 86 | FetchInput::Request(req) => { 87 | FetchInputHeap::Request(TracedHeap::new(req.reflector().get())) 88 | } 89 | FetchInput::Url(url) => FetchInputHeap::Url(url.to_string()), 90 | FetchInput::String(url) => FetchInputHeap::Url(url), 91 | }; 92 | 93 | unsafe { 94 | future_to_promise::<_, _, _, Exception>(cx, move |cx| async move { 95 | let (cx, http_req) = match input_heap { 96 | FetchInputHeap::Request(request_heap) => { 97 | let request = 98 | FetchRequest::get_mut_private(&cx, &request_heap.root(&cx).into()) 99 | .unwrap(); 100 | 101 | let mut http_req = http::Request::builder() 102 | .uri(request.get_url()) 103 | .method(request.method()); 104 | 105 | for header in request.headers(&cx) { 106 | http_req = http_req.header(header.0.clone(), header.1.clone()) 107 | } 108 | 109 | let request_body = request.take_body()?; 110 | let (cx, body_bytes) = 111 | cx.await_native_cx(|cx| request_body.into_bytes(cx)).await; 112 | let body_bytes = body_bytes?; 113 | let body = match body_bytes { 114 | Some(bytes) => hyper::Body::from(bytes), 115 | None => hyper::Body::empty(), 116 | }; 117 | 118 | (cx, http_req.body(body)?) 119 | } 120 | FetchInputHeap::Url(url) => ( 121 | cx, 122 | http::Request::builder() 123 | .uri(url) 124 | .method(http::Method::GET) 125 | .body(hyper::Body::empty())?, 126 | ), 127 | }; 128 | 129 | let (parts, body) = http_req.into_parts(); 130 | let request = super::super::Request { parts, body }; 131 | 132 | let url = url::Url::parse(request.parts.uri.to_string().as_str())?; 133 | let (cx, response) = cx 134 | .await_native(super::CloudflareRequestHandler::serve_static_file(request)) 135 | .await; 136 | let response = response.map_err(|e| { 137 | ion_mk_err!(format!("Failed to fetch static asset due to {e}"), Normal) 138 | })?; 139 | let response = FetchResponse::from_hyper_response(&cx, response, url)?; 140 | Ok(FetchResponse::new_object(&cx, Box::new(response))) 141 | }) 142 | } 143 | } 144 | } 145 | 146 | pub fn define(cx: &Context, global: &Object) -> bool { 147 | Env::init_class(cx, global).0 && EnvAssets::init_class(cx, global).0 148 | } 149 | -------------------------------------------------------------------------------- /src/request_handlers/cloudflare/routes.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{bail, Context, Result}; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize)] 7 | struct RoutesJson { 8 | version: u32, 9 | include: Vec, 10 | exclude: Vec, 11 | } 12 | 13 | pub struct Routes { 14 | include: Vec, 15 | exclude: Vec, 16 | } 17 | 18 | impl Routes { 19 | pub fn try_parse(dir: impl AsRef) -> Result> { 20 | let file_path = dir.as_ref().join("_routes.json"); 21 | let metadata = match std::fs::metadata(&file_path) { 22 | Ok(m) => m, 23 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), 24 | Err(e) => return Err(e).context("Failed to get metadata for _routes.json"), 25 | }; 26 | if !metadata.is_file() { 27 | bail!("Expected _routes.json to be a file"); 28 | } 29 | 30 | let file_content = 31 | std::fs::read_to_string(file_path).context("Failed to read _routes.json")?; 32 | let routes_json = serde_json::from_str::(&file_content) 33 | .context("Failed to parse _routes.json")?; 34 | 35 | if routes_json.version != 1 { 36 | bail!("Unsupported _routes.json version, only version 1 is supported"); 37 | } 38 | if routes_json.include.is_empty() { 39 | bail!("_routes.json must have at least one include rule"); 40 | } 41 | 42 | tracing::info!( 43 | "Read {} rules from _routes.json", 44 | routes_json.include.len() + routes_json.exclude.len() 45 | ); 46 | 47 | let wildcard_to_glob = |s: &String| s.replace('*', "**"); 48 | 49 | Ok(Some(Self { 50 | include: routes_json.include.iter().map(wildcard_to_glob).collect(), 51 | exclude: routes_json.exclude.iter().map(wildcard_to_glob).collect(), 52 | })) 53 | } 54 | 55 | pub fn should_route_to_function(&self, route: &str) -> bool { 56 | if self.include.is_empty() && self.exclude.is_empty() { 57 | // Special case for a missing _routes.json 58 | true 59 | } else if self 60 | .exclude 61 | .iter() 62 | .any(|p| glob_match::glob_match(p, route)) 63 | { 64 | false 65 | } else if self 66 | .include 67 | .iter() 68 | .any(|p| glob_match::glob_match(p, route)) 69 | { 70 | true 71 | } else { 72 | // This is the default when no match is found 73 | false 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/request_handlers/service_workers/event_listener.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use ion::{function_spec, Context, ErrorReport, Function, Object, PermanentHeap, Value}; 4 | use mozjs_sys::jsapi::{JSFunction, JSFunctionSpec}; 5 | 6 | thread_local! { 7 | static EVENT_CALLBACK: RefCell>> = RefCell::new(None); 8 | } 9 | 10 | #[js_fn] 11 | fn add_event_listener<'cx: 'f, 'f>(event: String, callback: Function<'f>) -> ion::Result<()> { 12 | if event != "fetch" { 13 | return Err(ion::Error::new( 14 | "Only the `fetch` event is supported", 15 | ion::ErrorKind::Type, 16 | )); 17 | } 18 | 19 | EVENT_CALLBACK.with(|cb| { 20 | let mut cb = cb.borrow_mut(); 21 | if cb.is_none() { 22 | *cb = Some(PermanentHeap::from_local(&callback)); 23 | Ok(()) 24 | } else { 25 | Err(ion::Error::new( 26 | "`fetch` event listener can only be registered once", 27 | ion::ErrorKind::Normal, 28 | )) 29 | } 30 | }) 31 | } 32 | 33 | pub fn invoke_fetch_event_callback<'cx>( 34 | cx: &'cx Context, 35 | args: &[Value], 36 | ) -> Result, Option> { 37 | let cb = EVENT_CALLBACK.with(|cb| { 38 | let cb = cb.borrow(); 39 | if cb.is_none() { 40 | Err(None) 41 | } else { 42 | Ok(Function::from(cb.as_ref().unwrap().root(cx))) 43 | } 44 | })?; 45 | cb.call(cx, &Object::global(cx), args) 46 | } 47 | 48 | static METHODS: &[JSFunctionSpec] = &[ 49 | function_spec!(add_event_listener, "addEventListener", 2), 50 | JSFunctionSpec::ZERO, 51 | ]; 52 | 53 | pub fn define(cx: &Context, global: &Object) -> bool { 54 | unsafe { global.define_methods(cx, METHODS) } 55 | } 56 | -------------------------------------------------------------------------------- /src/request_handlers/service_workers/fetch_event.rs: -------------------------------------------------------------------------------- 1 | use ion::Heap; 2 | use ion::{class::Reflector, ClassDefinition, Context, Promise}; 3 | use mozjs::jsapi::JSObject; 4 | 5 | #[js_class] 6 | pub struct FetchEvent { 7 | reflector: Reflector, 8 | pub(crate) request: Heap<*mut JSObject>, 9 | pub(crate) response: Option>, 10 | } 11 | 12 | impl FetchEvent { 13 | pub fn try_new(cx: &Context, request: super::super::Request) -> anyhow::Result { 14 | let request = Heap::new(super::super::build_fetch_request(cx, request)?); 15 | 16 | Ok(Self { 17 | reflector: Default::default(), 18 | request, 19 | response: None, 20 | }) 21 | } 22 | } 23 | 24 | #[js_class] 25 | impl FetchEvent { 26 | #[ion(constructor)] 27 | pub fn constructor() -> ion::Result { 28 | Err(ion::Error::new( 29 | "Cannot construct this class", 30 | ion::ErrorKind::Type, 31 | )) 32 | } 33 | 34 | #[ion(get)] 35 | pub fn get_request(&self) -> *mut JSObject { 36 | self.request.get() 37 | } 38 | 39 | #[ion(name = "respondWith")] 40 | pub fn respond_with(&mut self, cx: &Context, response: ion::Value) -> ion::Result<()> { 41 | match self.response { 42 | None => { 43 | if response.handle().is_object() { 44 | let obj = response.handle().to_object(); 45 | let rooted = cx.root(obj); 46 | if Promise::is_promise(&rooted) 47 | || runtime::globals::fetch::Response::instance_of(cx, &rooted.into()) 48 | { 49 | self.response = Some(Heap::new(obj)); 50 | Ok(()) 51 | } else { 52 | Err(ion::Error::new( 53 | "Value must be a promise or an instance of Response", 54 | ion::ErrorKind::Type, 55 | )) 56 | } 57 | } else { 58 | Err(ion::Error::new( 59 | "Value must be a promise or an instance of Response", 60 | ion::ErrorKind::Type, 61 | )) 62 | } 63 | } 64 | Some(_) => Err(ion::Error::new( 65 | "Response was already provided once", 66 | ion::ErrorKind::Normal, 67 | )), 68 | } 69 | } 70 | 71 | #[ion(name = "waitUntil")] 72 | pub fn wait_until(&self, _promise: ion::Promise) { 73 | // No need to do anything, the runtime will run the promise anyway 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/request_handlers/service_workers/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail}; 2 | use ion::{conversions::ToValue, ClassDefinition, Context, Object, Promise}; 3 | 4 | use crate::sm_utils::error_report_to_anyhow_error; 5 | 6 | use super::{Either, PendingResponse, ReadyResponse, Request}; 7 | 8 | pub mod event_listener; 9 | pub mod fetch_event; 10 | 11 | pub fn define(cx: &Context, global: &Object) -> bool { 12 | event_listener::define(cx, global) && fetch_event::FetchEvent::init_class(cx, global).0 13 | } 14 | 15 | pub fn start_request( 16 | cx: &Context, 17 | request: Request, 18 | ) -> anyhow::Result> { 19 | let fetch_event = Object::from(cx.root(fetch_event::FetchEvent::new_object( 20 | cx, 21 | Box::new(fetch_event::FetchEvent::try_new(cx, request)?), 22 | ))); 23 | 24 | let callback_rval = 25 | event_listener::invoke_fetch_event_callback(cx, &[fetch_event.as_value(cx)]).map_err( 26 | |e| { 27 | e.map(|e| error_report_to_anyhow_error(cx, e)) 28 | .unwrap_or(anyhow!("Script execution failed")) 29 | }, 30 | )?; 31 | 32 | if !callback_rval.get().is_undefined() { 33 | bail!("Script error: the fetch event handler should not return a value"); 34 | } 35 | 36 | let fetch_event = fetch_event::FetchEvent::get_private(cx, &fetch_event).unwrap(); 37 | 38 | match fetch_event.response.as_ref() { 39 | None => { 40 | bail!("Script error: FetchEvent.respondWith must be called with a Response object before returning") 41 | } 42 | Some(response) => { 43 | let response = Object::from(response.root(cx)); 44 | 45 | if Promise::is_promise(&response) { 46 | Ok(Either::Left(PendingResponse { 47 | promise: unsafe { Promise::from_unchecked(response.into_local()) }, 48 | })) 49 | } else { 50 | Ok(Either::Right(build_response(cx, response)?)) 51 | } 52 | } 53 | } 54 | } 55 | 56 | pub fn build_response(cx: &Context, value: Object) -> anyhow::Result { 57 | if !runtime::globals::fetch::Response::instance_of(cx, &value) { 58 | bail!("Script error: value provided to respondWith must be an instance of Response"); 59 | } 60 | 61 | let response = runtime::globals::fetch::Response::get_mut_private(cx, &value).unwrap(); 62 | 63 | super::build_response_from_fetch_response(cx, response) 64 | } 65 | -------------------------------------------------------------------------------- /src/request_handlers/wintercg.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use ion::{Context, Object, Value}; 3 | 4 | use crate::sm_utils; 5 | 6 | use super::{ 7 | ByRefStandardModules, Either, PendingResponse, ReadyResponse, Request, RequestHandler, UserCode, 8 | }; 9 | 10 | #[derive(Clone, Copy)] 11 | pub struct WinterCGRequestHandler; 12 | 13 | impl RequestHandler for WinterCGRequestHandler { 14 | fn get_standard_modules(&self) -> Box { 15 | Box::new(WinterCGStandardModules) 16 | } 17 | 18 | fn evaluate_scripts(&mut self, cx: &Context, code: &UserCode) -> Result<()> { 19 | match code { 20 | UserCode::Script { code, file_name } => { 21 | sm_utils::evaluate_script(cx, code, file_name)?; 22 | } 23 | UserCode::Module(path) => { 24 | sm_utils::evaluate_module(cx, path)?; 25 | } 26 | UserCode::Directory(_) => bail!("WinterCG mode does not support directories"), 27 | }; 28 | 29 | Ok(()) 30 | } 31 | 32 | fn start_handling_request( 33 | &mut self, 34 | cx: Context, 35 | request: Request, 36 | ) -> Result> { 37 | super::service_workers::start_request(&cx, request) 38 | } 39 | 40 | fn finish_fulfilled_request( 41 | &mut self, 42 | cx: Context, 43 | val: Value, 44 | ) -> Result> { 45 | if !val.handle().is_object() { 46 | bail!("Script error: value provided to respondWith was not an object"); 47 | } 48 | super::service_workers::build_response(&cx, val.to_object(&cx)).map(Either::Right) 49 | } 50 | } 51 | 52 | struct WinterCGStandardModules; 53 | 54 | impl ByRefStandardModules for WinterCGStandardModules { 55 | fn init_modules(&self, cx: &Context, global: &Object) -> bool { 56 | self.init_globals(cx, global) 57 | } 58 | 59 | fn init_globals(&self, cx: &Context, global: &Object) -> bool { 60 | super::service_workers::define(cx, global) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/runners/event_loop_stream.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{self, Poll}, 4 | }; 5 | 6 | use ion::ErrorReport; 7 | 8 | use crate::sm_utils::JsApp; 9 | 10 | /// This stream keeps stepping the event loop of its runtime, generating a 11 | /// value whenever the event loop is empty, but never finishing. 12 | pub struct EventLoopStream<'app> { 13 | pub(super) app: &'app JsApp, 14 | } 15 | 16 | impl<'app> futures::Stream for EventLoopStream<'app> { 17 | type Item = Result<(), Option>; 18 | 19 | fn poll_next(self: Pin<&mut Self>, wcx: &mut task::Context<'_>) -> Poll> { 20 | let rt = self.app.rt(); 21 | let event_loop_was_empty = rt.event_loop_is_empty(); 22 | match rt.step_event_loop(wcx) { 23 | Err(e) => Poll::Ready(Some(Err(e))), 24 | Ok(()) if rt.event_loop_is_empty() && !event_loop_was_empty => { 25 | Poll::Ready(Some(Ok(()))) 26 | } 27 | Ok(()) => Poll::Pending, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/runners/exec.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use anyhow::{Context, Result}; 4 | use tokio::task::LocalSet; 5 | 6 | use crate::{ 7 | builtins, 8 | sm_utils::{error_report_option_to_anyhow_error, evaluate_module, evaluate_script, JsApp}, 9 | }; 10 | 11 | async fn exec_script_inner(path: impl AsRef, script_mode: bool) -> Result<()> { 12 | let module_loader = (!script_mode).then(runtime::module::Loader::default); 13 | let standard_modules = builtins::Modules { 14 | include_internal: !script_mode, 15 | hardware_concurrency: 1, 16 | }; 17 | 18 | let js_app = JsApp::build(module_loader, Some(standard_modules)); 19 | let cx = js_app.cx(); 20 | let rt = js_app.rt(); 21 | 22 | if script_mode { 23 | let code = std::fs::read_to_string(&path).context("Failed to read script file")?; 24 | evaluate_script(cx, code, path.as_ref().as_os_str())?; 25 | } else { 26 | evaluate_module(cx, path)?; 27 | } 28 | 29 | rt.run_event_loop() 30 | .await 31 | .map_err(|e| error_report_option_to_anyhow_error(cx, e))?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn exec_script(path: PathBuf, script_mode: bool) -> Result<()> { 37 | // The top-level tokio runtime is *not* single-threaded, so we 38 | // need to spawn a new thread with a new single-threaded runtime 39 | // to run the JS code. 40 | std::thread::spawn(move || { 41 | tokio::runtime::Builder::new_current_thread() 42 | .enable_all() 43 | .build() 44 | .unwrap() 45 | .block_on(async move { 46 | let local_set = LocalSet::new(); 47 | local_set 48 | .run_until(exec_script_inner(path, script_mode)) 49 | .await 50 | }) 51 | }) 52 | .join() 53 | .unwrap() 54 | } 55 | -------------------------------------------------------------------------------- /src/runners/inline.rs: -------------------------------------------------------------------------------- 1 | //! An inline runner does not spawn new threads, instead executing 2 | //! requests inline on the task it's called on, which must be running 3 | //! in a single-threaded runtime. 4 | 5 | use std::{ 6 | sync::{ 7 | atomic::{AtomicBool, Ordering}, 8 | Arc, 9 | }, 10 | time::{Duration, Instant}, 11 | }; 12 | 13 | use anyhow::anyhow; 14 | use async_trait::async_trait; 15 | use futures::Future; 16 | use tokio::sync::mpsc; 17 | 18 | use crate::request_handlers::{RequestHandler, UserCode}; 19 | 20 | use super::{ 21 | request_loop::{handle_requests, ControlMessage, RequestData}, 22 | ResponseData, 23 | }; 24 | 25 | #[derive(Clone)] 26 | pub struct InlineRunner { 27 | channel: mpsc::UnboundedSender, 28 | finished: Arc, 29 | } 30 | 31 | pub trait InlineRunnerRequestHandlerFuture: Future {} 32 | 33 | impl> InlineRunnerRequestHandlerFuture for T {} 34 | 35 | impl InlineRunner { 36 | pub fn new_request_handler( 37 | handler: impl RequestHandler + Copy + Unpin, 38 | user_code: UserCode, 39 | ) -> (Self, impl InlineRunnerRequestHandlerFuture) { 40 | let (tx, rx) = mpsc::unbounded_channel(); 41 | let this = Self { 42 | channel: tx, 43 | finished: Arc::new(AtomicBool::new(false)), 44 | }; 45 | let finished_clone = this.finished.clone(); 46 | let fut = async move { 47 | handle_requests(handler, user_code, rx, 1).await; 48 | // Remember, we're running single-threaded, so no need 49 | // for any specific ordering logic. 50 | finished_clone.store(true, Ordering::Relaxed); 51 | }; 52 | (this, fut) 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl crate::server::Runner for InlineRunner { 58 | async fn handle( 59 | &self, 60 | _addr: std::net::SocketAddr, 61 | req: http::request::Parts, 62 | body: hyper::Body, 63 | ) -> Result, anyhow::Error> { 64 | let (tx, rx) = tokio::sync::oneshot::channel(); 65 | 66 | self.channel.send(ControlMessage::HandleRequest( 67 | RequestData { _addr, req, body }, 68 | tx, 69 | ))?; 70 | 71 | let response = rx.await?; 72 | 73 | // TODO: handle script errors 74 | match response { 75 | ResponseData::Done(resp) => Ok(resp), 76 | ResponseData::RequestError(err) => Err(err), 77 | ResponseData::ScriptError(err) => { 78 | if let Some(err) = err { 79 | println!("{err:?}"); 80 | } 81 | Err(anyhow!("Error encountered while evaluating user script")) 82 | } 83 | } 84 | } 85 | 86 | async fn shutdown(&self, timeout: Option) { 87 | tracing::info!("Shutting down..."); 88 | 89 | if self.channel.send(ControlMessage::Shutdown).is_err() { 90 | // Channel already closed, future must have run to completion 91 | return; 92 | } 93 | 94 | let shutdown_started = Instant::now(); 95 | 96 | loop { 97 | if !self.finished.load(Ordering::Relaxed) { 98 | if let Some(timeout) = timeout { 99 | if shutdown_started.elapsed() >= timeout { 100 | tracing::warn!( 101 | "Clean shutdown timeout was reached before all \ 102 | requests could finish processing" 103 | ); 104 | let _ = self.channel.send(ControlMessage::Terminate); 105 | break; 106 | } 107 | } 108 | 109 | tracing::debug!("Still waiting for threads to quit..."); 110 | tokio::time::sleep(Duration::from_secs(1)).await; 111 | } else { 112 | break; 113 | } 114 | } 115 | 116 | tracing::info!( 117 | "Shutdown completed in {} seconds", 118 | shutdown_started.elapsed().as_secs() 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/runners/mod.rs: -------------------------------------------------------------------------------- 1 | mod event_loop_stream; 2 | pub mod exec; 3 | pub mod inline; 4 | mod request_loop; 5 | mod request_queue; 6 | pub mod single; 7 | pub mod watch; 8 | 9 | #[derive(Debug)] 10 | pub enum ResponseData { 11 | Done(hyper::Response), 12 | RequestError(anyhow::Error), 13 | 14 | // The error can only be returned once, so future calls to the 15 | // thread will return None instead 16 | ScriptError(Option), 17 | } 18 | -------------------------------------------------------------------------------- /src/runners/request_queue.rs: -------------------------------------------------------------------------------- 1 | use std::{pin::Pin, task::Poll}; 2 | 3 | use futures::{future::Fuse, stream::FuturesUnordered, Future, FutureExt, Stream, StreamExt}; 4 | use ion::{PromiseFuture, TracedHeap}; 5 | use mozjs::{jsapi::JSContext, jsval::JSVal}; 6 | 7 | use crate::request_handlers::PendingResponse; 8 | 9 | pub trait RequestFinishedHandler: Unpin { 10 | type CancelReason: Unpin + Copy; 11 | 12 | fn request_finished( 13 | &mut self, 14 | result: Result, TracedHeap>, 15 | ) -> RequestFinishedResult; 16 | 17 | fn request_cancelled(&mut self, reason: Self::CancelReason); 18 | } 19 | 20 | pub struct RequestQueue { 21 | cx: *mut JSContext, 22 | requests: FuturesUnordered>, 23 | continuations: FuturesUnordered>>>, 24 | } 25 | 26 | impl RequestQueue { 27 | pub fn new(cx: &ion::Context) -> Self { 28 | Self { 29 | cx: cx.as_ptr(), 30 | requests: FuturesUnordered::new(), 31 | continuations: FuturesUnordered::new(), 32 | } 33 | } 34 | 35 | pub fn is_empty(&self) -> bool { 36 | self.requests.is_empty() && self.continuations.is_empty() 37 | } 38 | 39 | pub fn push(&mut self, pending: PendingResponse, on_finished: F) { 40 | self.requests.push(RequestFuture { 41 | promise: PromiseFuture::new( 42 | unsafe { ion::Context::new_unchecked(self.cx) }, 43 | &pending.promise, 44 | ) 45 | .fuse(), 46 | on_finished, 47 | }) 48 | } 49 | 50 | pub fn push_continuation(&mut self, future: Pin>>) { 51 | self.continuations.push(future); 52 | } 53 | 54 | pub fn cancel_all(&mut self, cancel_reason: F::CancelReason) { 55 | let mut requests = FuturesUnordered::new(); 56 | std::mem::swap(&mut requests, &mut self.requests); 57 | for mut req in requests.into_iter() { 58 | req.on_finished.request_cancelled(cancel_reason); 59 | } 60 | } 61 | 62 | pub fn cancel_unfinished(&mut self, cancel_reason: F::CancelReason) -> CancelUnfinished<'_, F> { 63 | CancelUnfinished { 64 | queue: self, 65 | reason: cancel_reason, 66 | } 67 | } 68 | } 69 | 70 | impl Stream for RequestQueue { 71 | type Item = (); 72 | 73 | fn poll_next( 74 | mut self: Pin<&mut Self>, 75 | cx: &mut std::task::Context<'_>, 76 | ) -> std::task::Poll> { 77 | if self.is_empty() { 78 | return Poll::Pending; 79 | } 80 | 81 | if !self.requests.is_empty() { 82 | if let Poll::Ready(req) = self.requests.poll_next_unpin(cx) { 83 | match req { 84 | None | Some(None) => (), 85 | Some(Some(future)) => self.continuations.push(future), 86 | } 87 | 88 | return Poll::Ready(Some(())); 89 | } 90 | } 91 | 92 | if !self.continuations.is_empty() && self.continuations.poll_next_unpin(cx).is_ready() { 93 | return Poll::Ready(Some(())); 94 | } 95 | 96 | Poll::Pending 97 | } 98 | } 99 | 100 | pub struct CancelUnfinished<'q, F: RequestFinishedHandler> { 101 | queue: &'q mut RequestQueue, 102 | reason: F::CancelReason, 103 | } 104 | 105 | impl<'q, F: RequestFinishedHandler> Future for CancelUnfinished<'q, F> { 106 | type Output = (); 107 | 108 | fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { 109 | let this = self.get_mut(); 110 | while this.queue.poll_next_unpin(cx).is_ready() { 111 | // Nothing to do, we're just letting all ready things finish 112 | } 113 | this.queue.cancel_all(this.reason); 114 | Poll::Ready(()) 115 | } 116 | } 117 | 118 | struct RequestFuture { 119 | promise: Fuse, 120 | on_finished: F, 121 | } 122 | 123 | pub enum RequestFinishedResult { 124 | Pending(ion::Promise), 125 | HasContinuation(Pin>>), 126 | Done, 127 | } 128 | 129 | impl Future for RequestFuture { 130 | type Output = Option>>>; 131 | 132 | fn poll(mut self: Pin<&mut Self>, wcx: &mut std::task::Context<'_>) -> Poll { 133 | match self.promise.poll_unpin(wcx) { 134 | Poll::Pending => Poll::Pending, 135 | Poll::Ready((cx, res)) => match self.on_finished.request_finished(res) { 136 | RequestFinishedResult::Done => Poll::Ready(None), 137 | RequestFinishedResult::HasContinuation(future) => Poll::Ready(Some(future)), 138 | RequestFinishedResult::Pending(promise) => { 139 | self.promise = PromiseFuture::new(cx, &promise).fuse(); 140 | self.poll_unpin(wcx) 141 | } 142 | }, 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/runners/watch.rs: -------------------------------------------------------------------------------- 1 | // TODO: get watch mode working again 2 | 3 | // use std::{path::PathBuf, sync::Arc}; 4 | 5 | // use anyhow::bail; 6 | // use tokio::sync::Mutex; 7 | 8 | // use crate::request_handlers::{RequestHandler, UserCode}; 9 | 10 | // use super::single::{SharedSingleRunner, SingleRunner}; 11 | 12 | // /// Wraps an [`SingleRunner`] with auto-reload capabilities. 13 | // /// 14 | // /// Will auto-reload the JS code, and re-initialize the runner on changes. 15 | // #[derive(Clone)] 16 | // pub struct WatchRunner { 17 | // state: Arc, 18 | // } 19 | 20 | // struct State { 21 | // js_path: PathBuf, 22 | // handler: Box, 23 | // max_js_threads: usize, 24 | // mutable: Mutex>, 25 | // } 26 | 27 | // struct MutableState { 28 | // contents: String, 29 | // runner: SharedSingleRunner, 30 | // } 31 | 32 | // // TODO: support watching modules. This is complicated by the fact that we must 33 | // // watch all modules loaded in after the entry module was loaded as well. 34 | // impl WatchRunner { 35 | // pub fn new( 36 | // handler: Box, 37 | // js_path: PathBuf, 38 | // script_mode: bool, 39 | // max_js_threads: usize, 40 | // ) -> anyhow::Result { 41 | // if !script_mode { 42 | // bail!("Watch mode is not compatible with module mode yet. Use --script to run in script mode instead."); 43 | // } 44 | // Ok(Self { 45 | // state: Arc::new(State { 46 | // handler, 47 | // js_path, 48 | // max_js_threads, 49 | // mutable: Mutex::new(None), 50 | // }), 51 | // }) 52 | // } 53 | 54 | // async fn acquire_runner(&self) -> Result { 55 | // // WASIX does not support inotify file watching APIs, so we naively 56 | // // load the full file contents on every request. 57 | // // 58 | // // This is slow, but this should be sufficient for debugging. 59 | // // 60 | // // We should investigate just going by the file modification time. 61 | // // but loading the full contents is more reliable. 62 | 63 | // let mut mutable = self.state.mutable.lock().await; 64 | 65 | // let (contents, file_name) = match UserCode::from_path(&self.state.js_path, true) 66 | // .await 67 | // .unwrap() 68 | // { 69 | // UserCode::Script { code, file_name } => (code, file_name), 70 | // _ => unreachable!(), 71 | // }; 72 | 73 | // if let Some(mutable) = mutable.as_mut() { 74 | // if mutable.contents == contents { 75 | // return Ok(mutable.runner.clone()); 76 | // } 77 | // } 78 | 79 | // tracing::info!(path=%self.state.js_path.display(), "reloaded application code"); 80 | 81 | // let runner = SingleRunner::new_request_handler( 82 | // self.state.handler.clone(), 83 | // self.state.max_js_threads, 84 | // UserCode::Script { 85 | // code: contents.clone(), 86 | // file_name, 87 | // }, 88 | // ); 89 | // *mutable = Some(MutableState { 90 | // contents, 91 | // runner: runner.clone(), 92 | // }); 93 | 94 | // Ok(runner) 95 | // } 96 | // } 97 | 98 | // #[async_trait::async_trait] 99 | // impl crate::server::Runner for WatchRunner { 100 | // async fn handle( 101 | // &self, 102 | // addr: std::net::SocketAddr, 103 | // req: http::request::Parts, 104 | // body: hyper::Body, 105 | // ) -> Result, anyhow::Error> { 106 | // let runner = self.acquire_runner().await?; 107 | // runner.handle(addr, req, body).await 108 | // } 109 | // } 110 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::net::SocketAddr; 3 | use std::time::Duration; 4 | 5 | use anyhow::Context as _; 6 | use async_trait::async_trait; 7 | use hyper::server::conn::AddrStream; 8 | use hyper::service::{make_service_fn, service_fn}; 9 | use hyper::{Body, Request, Response, Server}; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct ServerConfig { 13 | pub addr: SocketAddr, 14 | } 15 | 16 | pub async fn run_server( 17 | config: ServerConfig, 18 | handler: BoxedDynRunner, 19 | shutdown_signal: tokio::sync::oneshot::Receiver<()>, 20 | ) -> Result<(), anyhow::Error> { 21 | let context = AppContext { runner: handler }; 22 | 23 | let make_service = make_service_fn(move |conn: &AddrStream| { 24 | let context = context.clone(); 25 | 26 | let addr = conn.remote_addr(); 27 | 28 | // Create a `Service` for responding to the request. 29 | let service = service_fn(move |req| handle(context.clone(), addr, req)); 30 | 31 | // Return the service to hyper. 32 | async move { Ok::<_, Infallible>(service) } 33 | }); 34 | 35 | let addr = config.addr; 36 | tracing::info!(listen=%addr, "starting server on '{addr}'"); 37 | 38 | Server::bind(&addr) 39 | .serve(make_service) 40 | .with_graceful_shutdown(async move { _ = shutdown_signal.await }) 41 | .await 42 | .context("hyper server failed") 43 | } 44 | 45 | #[async_trait] 46 | #[dyn_clonable::clonable] 47 | pub trait Runner: Send + Sync + Clone + 'static { 48 | async fn handle( 49 | &self, 50 | addr: SocketAddr, 51 | req: http::request::Parts, 52 | body: hyper::Body, 53 | ) -> anyhow::Result>; 54 | 55 | async fn shutdown(&self, timeout: Option); 56 | } 57 | 58 | pub type BoxedDynRunner = Box; 59 | 60 | #[derive(Clone)] 61 | struct AppContext { 62 | runner: BoxedDynRunner, 63 | } 64 | 65 | async fn handle( 66 | context: AppContext, 67 | addr: SocketAddr, 68 | req: Request, 69 | ) -> Result, Infallible> { 70 | let res = match handle_inner(context, addr, req).await { 71 | Ok(r) => r, 72 | Err(err) => { 73 | tracing::error!(error = format!("{err:#?}"), "could not process request"); 74 | 75 | hyper::Response::builder() 76 | .status(hyper::StatusCode::INTERNAL_SERVER_ERROR) 77 | .body(hyper::Body::from(err.to_string())) 78 | .unwrap() 79 | } 80 | }; 81 | 82 | Ok(res) 83 | } 84 | 85 | async fn handle_inner( 86 | context: AppContext, 87 | addr: SocketAddr, 88 | req: Request, 89 | ) -> Result, anyhow::Error> { 90 | let (parts, body) = req.into_parts(); 91 | context 92 | .runner 93 | .handle(addr, parts, body) 94 | .await 95 | .context("JavaScript failed") 96 | } 97 | -------------------------------------------------------------------------------- /src/sm_utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, path::Path}; 2 | 3 | use anyhow::{anyhow, Context as _}; 4 | use ion::{module::ModuleLoader, Context, ErrorReport}; 5 | use mozjs::{ 6 | jsapi::WeakRefSpecifier, 7 | rust::{JSEngine, JSEngineHandle, RealmOptions}, 8 | }; 9 | use runtime::{module::StandardModules, Runtime, RuntimeBuilder}; 10 | use self_cell::self_cell; 11 | 12 | pub static ENGINE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { 13 | let engine = JSEngine::init().expect("could not create engine"); 14 | let handle = engine.handle(); 15 | std::mem::forget(engine); 16 | handle 17 | }); 18 | 19 | #[macro_export] 20 | macro_rules! ion_mk_err { 21 | ($msg:expr, $ty:ident) => { 22 | ion::Error::new($msg, ion::ErrorKind::$ty) 23 | }; 24 | } 25 | 26 | #[macro_export] 27 | macro_rules! ion_err { 28 | ($msg:expr, $ty:ident) => { 29 | return Err($crate::ion_mk_err!($msg, $ty)) 30 | }; 31 | } 32 | 33 | pub struct ContextWrapper { 34 | // Important: the context must come first, because it has to be dropped 35 | // before the runtime, otherwise we get a nasty error at runtime 36 | cx: Context, 37 | _rt: mozjs::rust::Runtime, 38 | } 39 | 40 | self_cell!( 41 | pub struct JsApp { 42 | owner: ContextWrapper, 43 | 44 | #[covariant] 45 | dependent: Runtime, 46 | } 47 | ); 48 | 49 | impl JsApp { 50 | pub fn build( 51 | loader: Option, 52 | modules: Option, 53 | ) -> Self { 54 | let rt = mozjs::rust::Runtime::new(ENGINE.clone()); 55 | let cx = Context::from_runtime(&rt); 56 | let wrapper = ContextWrapper { _rt: rt, cx }; 57 | Self::new(wrapper, |w| Self::create_runtime(w, loader, modules)) 58 | } 59 | 60 | pub fn cx(&self) -> &Context { 61 | self.borrow_dependent().cx() 62 | } 63 | 64 | pub fn rt(&self) -> &Runtime { 65 | self.borrow_dependent() 66 | } 67 | 68 | fn create_runtime( 69 | wrapper: &ContextWrapper, 70 | loader: Option, 71 | modules: Option, 72 | ) -> Runtime { 73 | let mut realm_options = RealmOptions::default(); 74 | realm_options.creationOptions_.streams_ = true; 75 | realm_options.creationOptions_.weakRefs_ = WeakRefSpecifier::EnabledWithCleanupSome; 76 | let rt_builder = RuntimeBuilder::::new() 77 | .microtask_queue() 78 | .macrotask_queue() 79 | .realm_options(realm_options); 80 | 81 | let rt_builder = match loader { 82 | Some(loader) => rt_builder.modules(loader), 83 | None => rt_builder, 84 | }; 85 | 86 | let rt_builder = match modules { 87 | Some(modules) => rt_builder.standard_modules(modules), 88 | None => rt_builder, 89 | }; 90 | 91 | rt_builder.build(&wrapper.cx) 92 | } 93 | } 94 | 95 | pub fn evaluate_script( 96 | cx: &Context, 97 | code: impl AsRef, 98 | file_name: impl AsRef, 99 | ) -> anyhow::Result { 100 | ion::script::Script::compile_and_evaluate(cx, Path::new(&file_name), code.as_ref()) 101 | .map_err(|e| error_report_to_anyhow_error(cx, e)) 102 | } 103 | 104 | pub fn evaluate_module( 105 | cx: &Context, 106 | path: impl AsRef, 107 | ) -> anyhow::Result { 108 | let path = path.as_ref(); 109 | 110 | let file_name = path 111 | .file_name() 112 | .ok_or(anyhow!("Failed to get file name from script path")) 113 | .map(|f| f.to_string_lossy().into_owned())?; 114 | 115 | let code = std::fs::read_to_string(path).context("Failed to read script file")?; 116 | 117 | Ok( 118 | ion::module::Module::compile_and_evaluate(cx, &file_name, Some(path), &code) 119 | .map_err(|e| { 120 | error_report_option_to_anyhow_error(cx, Some(e.report)).context(format!( 121 | "Error while loading module during {:?} step", 122 | e.kind 123 | )) 124 | })? 125 | .0, 126 | ) 127 | } 128 | 129 | pub fn error_report_to_anyhow_error(cx: &Context, error_report: ErrorReport) -> anyhow::Error { 130 | match error_report.stack { 131 | Some(stack) => anyhow::anyhow!( 132 | "Script error: {}\nat:\n{}", 133 | error_report.exception.format(cx), 134 | stack.format() 135 | ), 136 | None => anyhow::anyhow!("Runtime error: {}", error_report.exception.format(cx)), 137 | } 138 | } 139 | 140 | pub fn error_report_option_to_anyhow_error( 141 | cx: &Context, 142 | error_report: Option, 143 | ) -> anyhow::Error { 144 | match error_report { 145 | Some(e) => error_report_to_anyhow_error(cx, e), 146 | None => anyhow!("Unknown script error"), 147 | } 148 | } 149 | 150 | // We can't take a list of modules because StandardModules::init takes self by value, which 151 | // means that Vec is out of the question. 152 | pub struct TwoStandardModules(pub M1, pub M2); 153 | 154 | impl StandardModules for TwoStandardModules { 155 | fn init(self, cx: &ion::Context, global: &ion::Object) -> bool { 156 | self.0.init(cx, global) && self.1.init(cx, global) 157 | } 158 | 159 | fn init_globals(self, cx: &ion::Context, global: &ion::Object) -> bool { 160 | self.0.init_globals(cx, global) && self.1.init_globals(cx, global) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test-suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-suite" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.75" 10 | async-trait = "0.1.74" 11 | clap = "4.4.12" 12 | futures = "0.3.30" 13 | libtest-mimic = "0.6.1" 14 | pretty_assertions = "1.4.0" 15 | reqwest = { version = "0.11.22", default-features = false, features = [ 16 | "json", 17 | "rustls-tls", 18 | "stream", 19 | "multipart", 20 | ] } 21 | serde = { version = "1.0.192", features = ["derive"] } 22 | tokio = { version = "=1.24.2", features = ["full"] } 23 | toml = "0.8.8" 24 | -------------------------------------------------------------------------------- /test-suite/js-test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .wrangler/* 27 | -------------------------------------------------------------------------------- /test-suite/js-test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-test-app", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "node src/main.js", 7 | "build": "rollup -c", 8 | "build-local": "rollup -c --local", 9 | "run": "node dist/bundle.js" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^25.0.7", 13 | "@rollup/plugin-node-resolve": "^15.2.3", 14 | "@rollup/plugin-replace": "^5.0.5", 15 | "@rollup/plugin-terser": "^0.4.4", 16 | "rollup": "^4.3.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-suite/js-test-app/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import replace from '@rollup/plugin-replace'; 4 | import terser from "@rollup/plugin-terser"; 5 | 6 | export default cliArgs => { 7 | return { 8 | input: "src/main.js", // the entry point of your application 9 | output: { 10 | file: "dist/bundle.js", // the output bundle 11 | format: "esm", // the output format 12 | }, 13 | plugins: [ 14 | resolve(), // resolves node modules 15 | commonjs(), // converts commonjs to es modules 16 | replace({ 17 | preventAssignment: true, 18 | 'process.env.TESTS_BACKEND_URL': cliArgs.local ? "'http://localhost:8081/'" : "'https://winter-tests.wasmer.app/'" 19 | }), 20 | terser(), // minifies the bundle 21 | ], 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/main.js: -------------------------------------------------------------------------------- 1 | import { handleRequest as handleHello } from "./test-files/1-hello.js"; 2 | import { handleRequest as handleBlob } from "./test-files/2-blob.js"; 3 | import { handleRequest as handleFile } from "./test-files/2.1-file.js"; 4 | import { handleRequest as handleHeaders } from "./test-files/3-headers.js"; 5 | import { handleRequest as handleRequest } from "./test-files/4-request.js"; 6 | import { handleRequest as handleResponse } from "./test-files/5-response.js"; 7 | import { handleRequest as handleTextEncoder } from "./test-files/6-text-encoder.js"; 8 | import { handleRequest as handleTextDecoder } from "./test-files/7-text-decoder.js"; 9 | import { handleRequest as handleURL } from "./test-files/8-url.js"; 10 | import { handleRequest as handleSearchParams } from "./test-files/8.1-search-params.js"; 11 | import { handleRequest as handleAtobBtoA } from "./test-files/10-atob-btoa.js"; 12 | import { handleRequest as handleFetch } from "./test-files/11-fetch.js"; 13 | import { handleRequest as handleFetchBody } from "./test-files/11.1-fetch-body.js"; 14 | import { handleRequest as handleStreams } from "./test-files/12-streams.js"; 15 | import { handleRequest as handleTransformStream } from "./test-files/12.1-transform-stream.js"; 16 | import { handleRequest as handleTextEncoderStream } from "./test-files/12.2-text-encoder-stream.js"; 17 | import { handleRequest as handleTextDecoderStream } from "./test-files/12.3-text-decoder-stream.js"; 18 | import { handleRequest as handlePerformance } from "./test-files/13-performance.js"; 19 | import { handleRequest as handleFormData } from "./test-files/14-form-data.js"; 20 | import { handleRequest as handleTimers } from "./test-files/15-timers.js"; 21 | import { handleRequest as handleCrypto } from "./test-files/16-crypto.js"; 22 | import { handleRequest as handleCryptoHmac } from "./test-files/16.1-crypto-hmac.js"; 23 | import { handleRequest as handleCryptoSha } from "./test-files/16.2-crypto-sha.js"; 24 | import { handleRequest as handleCache } from "./test-files/17-cache.js"; 25 | import { handleRequest as handleEvent } from "./test-files/18-event.js"; 26 | import { handleRequest as handleAbort } from "./test-files/19-abort.js"; 27 | 28 | function router(req) { 29 | const url = new URL(req.url); 30 | const path = url.pathname; 31 | 32 | if (path.startsWith("/1-hello")) { 33 | return handleHello(req); 34 | } 35 | if (path.startsWith("/2-blob")) { 36 | return handleBlob(req); 37 | } 38 | if (path.startsWith("/2.1-file")) { 39 | return handleFile(req); 40 | } 41 | if (path.startsWith("/3-headers")) { 42 | return handleHeaders(req); 43 | } 44 | if (path.startsWith("/4-request")) { 45 | return handleRequest(req); 46 | } 47 | if (path.startsWith("/5-response")) { 48 | return handleResponse(req); 49 | } 50 | if (path.startsWith("/6-text-encoder")) { 51 | return handleTextEncoder(req); 52 | } 53 | if (path.startsWith("/7-text-decoder")) { 54 | return handleTextDecoder(req); 55 | } 56 | if (path.startsWith("/8-url")) { 57 | return handleURL(req); 58 | } 59 | if (path.startsWith("/8.1-search-params")) { 60 | return handleSearchParams(req); 61 | } 62 | if (path.startsWith("/10-atob-btoa")) { 63 | return handleAtobBtoA(req); 64 | } 65 | if (path.startsWith("/11-fetch")) { 66 | return handleFetch(req); 67 | } 68 | if (path.startsWith("/11.1-fetch-body")) { 69 | return handleFetchBody(req); 70 | } 71 | if (path.startsWith("/12-streams")) { 72 | return handleStreams(req); 73 | } 74 | if (path.startsWith("/12.1-transform-stream")) { 75 | return handleTransformStream(req); 76 | } 77 | if (path.startsWith("/12.2-text-encoder-stream")) { 78 | return handleTextEncoderStream(req); 79 | } 80 | if (path.startsWith("/12.3-text-decoder-stream")) { 81 | return handleTextDecoderStream(req); 82 | } 83 | if (path.startsWith("/13-performance")) { 84 | return handlePerformance(req); 85 | } 86 | if (path.startsWith("/14-form-data")) { 87 | return handleFormData(req); 88 | } 89 | if (path.startsWith("/15-timers")) { 90 | return handleTimers(req); 91 | } 92 | if (path.startsWith("/16-crypto")) { 93 | return handleCrypto(req); 94 | } 95 | if (path.startsWith("/16.1-crypto-hmac")) { 96 | return handleCryptoHmac(req); 97 | } 98 | if (path.startsWith("/16.2-crypto-sha")) { 99 | return handleCryptoSha(req); 100 | } 101 | if (path.startsWith("/17-cache")) { 102 | return handleCache(req); 103 | } 104 | if (path.startsWith("/18-event")) { 105 | return handleEvent(req); 106 | } 107 | if (path.startsWith("/19-abort")) { 108 | return handleAbort(req); 109 | } 110 | return new Response(`Route Not Found - ${path}`, { status: 404 }); 111 | } 112 | 113 | addEventListener("fetch", (fetchEvent) => { 114 | fetchEvent.respondWith(router(fetchEvent.request)); 115 | }); 116 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/1-hello.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(req) { 2 | return new Response("hello"); 3 | } 4 | 5 | export { handleRequest }; 6 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/10-atob-btoa.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | const assert = (condition, message) => { 3 | if (!condition) { 4 | throw new Error(message || "Assertion failed"); 5 | } 6 | }; 7 | 8 | const assertEquals = (actual, expected, message) => { 9 | assert( 10 | actual === expected, 11 | message || `Expected ${expected} but got ${actual}` 12 | ); 13 | }; 14 | try { 15 | const string = "Hello, world!"; 16 | const base64Encoded = "SGVsbG8sIHdvcmxkIQ=="; 17 | 18 | // Test btoa 19 | const encoded = btoa(string); 20 | assertEquals( 21 | encoded, 22 | base64Encoded, 23 | "btoa did not encode the string correctly" 24 | ); 25 | 26 | // Test atob 27 | const decoded = atob(base64Encoded); 28 | assertEquals(decoded, string, "atob did not decode the string correctly"); 29 | 30 | // Test btoa with binary data 31 | try { 32 | const binaryData = "\x00\x01\x02"; 33 | btoa(binaryData); 34 | assert(true, "btoa handled binary data without throwing error"); 35 | } catch (e) { 36 | assert(false, "btoa should not throw error with binary data"); 37 | } 38 | 39 | // Test atob with invalid input 40 | try { 41 | atob("Invalid base64 string"); 42 | assert(false, "atob should throw error with invalid base64 input"); 43 | } catch (e) { 44 | assert(true, "atob threw error as expected with invalid base64 input"); 45 | } 46 | // Create a response with the Blob's text 47 | return new Response("All tests passed!", { 48 | headers: { "content-type": "text/plain" }, 49 | }); 50 | } catch (error) { 51 | // If there's an error, return the error message in the response 52 | return new Response(error.message, { status: 500 }); 53 | } 54 | } 55 | 56 | export { handleRequest }; 57 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/11.1-fetch-body.js: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | assert_equals, 4 | promise_test 5 | } from "../test-utils.js"; 6 | 7 | const multipartBody = 8 | `--BOUNDARY\r\n` + 9 | `Content-Disposition: form-data; name="a"\r\n` + 10 | `\r\n` + 11 | `b\r\n` + 12 | `--BOUNDARY\r\n` + 13 | `Content-Disposition: form-data; name="c"; filename="d.txt"\r\n` + 14 | `Content-Type: text/plain\r\n` + 15 | `\r\n` + 16 | `e, f, g\r\n` + 17 | `h, i, j\r\n` + 18 | `--BOUNDARY--\r\n`; 19 | 20 | async function handleRequest(request) { 21 | try { 22 | await promise_test(async () => { 23 | let response = new Response("1234"); 24 | 25 | let body = await response.arrayBuffer(); 26 | let td = new TextDecoder(); 27 | assert(body instanceof ArrayBuffer); 28 | assert_equals(td.decode(body), "1234"); 29 | }, "arrayBuffer"); 30 | 31 | await promise_test(async () => { 32 | let response = new Response("1234"); 33 | 34 | let body = await response.blob(); 35 | assert(body instanceof Blob); 36 | assert_equals(await body.text(), "1234"); 37 | }, "blob"); 38 | 39 | await promise_test(async () => { 40 | let response = new Response(JSON.stringify({ a: "b", c: "d" })); 41 | 42 | let body = await response.json(); 43 | assert_equals(body.a, "b"); 44 | assert_equals(body.c, "d"); 45 | }, "json"); 46 | 47 | await promise_test(async () => { 48 | let response = new Response("a=b&c=d", { 49 | headers: { 50 | "Content-Type": "application/x-www-form-urlencoded" 51 | } 52 | }); 53 | 54 | let formData = await response.formData(); 55 | assert_equals(formData.get('a'), 'b'); 56 | assert_equals(formData.get('c'), 'd'); 57 | }, "x-www-form-urlencoded body"); 58 | 59 | await promise_test(async () => { 60 | let response = new Response(multipartBody, { 61 | headers: { 62 | "Content-Type": "multipart/form-data; boundary=BOUNDARY" 63 | } 64 | }); 65 | 66 | let formData = await response.formData(); 67 | 68 | let a = formData.get('a'); 69 | assert_equals(typeof(a), "string"); 70 | assert_equals(a, 'b'); 71 | 72 | let c = formData.get('c'); 73 | assert(c instanceof File, 'c must be a File'); 74 | assert_equals(c.name, 'd.txt'); 75 | assert_equals(c.type, "text/plain"); 76 | assert_equals(await c.text(), "e, f, g\r\nh, i, j"); 77 | }, "multipart/form-data body"); 78 | 79 | return new Response("All tests passed!", { 80 | headers: { "content-type": "text/plain" }, 81 | }); 82 | } catch (error) { 83 | return new Response(error.message, { status: 500 }); 84 | } 85 | } 86 | 87 | export { handleRequest }; -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/12-streams.js: -------------------------------------------------------------------------------- 1 | import { assert_array_equals, readableStreamToArray } from "../test-utils"; 2 | 3 | async function handleRequest(request) { 4 | const assert = (condition, message) => { 5 | if (!condition) { 6 | throw new Error(message || "Assertion failed"); 7 | } 8 | }; 9 | 10 | const assertEquals = (actual, expected, message) => { 11 | assert( 12 | actual === expected, 13 | message || `Expected ${expected} but got ${actual}` 14 | ); 15 | }; 16 | try { 17 | try { 18 | // Create a readable stream 19 | const readableStream = new ReadableStream({ 20 | start(controller) { 21 | controller.enqueue("Hello, "); 22 | controller.enqueue("Wasmer!"); 23 | controller.close(); 24 | }, 25 | }); 26 | 27 | // Read from the stream 28 | const reader = readableStream.getReader(); 29 | let result = ""; 30 | 31 | while (true) { 32 | const { done, value } = await reader.read(); 33 | if (done) break; 34 | result += value; 35 | } 36 | 37 | assertEquals( 38 | result, 39 | "Hello, Wasmer!", 40 | `Unexpected result from readable stream. Expected 'Hello, Wasmer!' but got ${result}` 41 | ); 42 | } catch (error) { 43 | assert(false, `ReadableStream test failed: ${error}`); 44 | } 45 | 46 | try { 47 | let accumulatedData = ""; 48 | const writableStream = new WritableStream({ 49 | write(chunk) { 50 | accumulatedData += chunk; 51 | }, 52 | close() { 53 | accumulatedData += "!"; 54 | }, 55 | }); 56 | 57 | const writer = writableStream.getWriter(); 58 | writer.write("Hello,"); 59 | writer.write(" "); 60 | writer.write("Wasmer"); 61 | await writer.close(); 62 | 63 | assertEquals( 64 | accumulatedData, 65 | "Hello, Wasmer!", 66 | `Unexpected result from writable stream. Expected 'Hello, Wasmer!' but got ${accumulatedData}` 67 | ); 68 | } catch (error) { 69 | assert(false, `WritableStream test failed: ${error}`); 70 | } 71 | 72 | // Testing the ReadableStream Backpressure 73 | try { 74 | let readCount = 0; 75 | const readableStream = new ReadableStream({ 76 | start(controller) { 77 | controller.enqueue("A"); 78 | controller.enqueue("B"); 79 | controller.enqueue("C"); 80 | // Simulate a delay for the next enqueue 81 | setTimeout(() => controller.enqueue("D"), 500); 82 | }, 83 | pull(controller) { 84 | readCount++; 85 | if (readCount > 3) { 86 | controller.close(); 87 | } 88 | }, 89 | }); 90 | 91 | const reader = readableStream.getReader(); 92 | let result = ""; 93 | 94 | while (true) { 95 | const { done, value } = await reader.read(); 96 | if (done) break; 97 | result += value; 98 | } 99 | 100 | assertEquals( 101 | result, 102 | "ABCD", 103 | `Backpressure test failed. Expected 'ABCD' but got ${result}` 104 | ); 105 | } catch (error) { 106 | assert(false, `ReadableStream backpressure test failed: ${error}`); 107 | } 108 | 109 | // Testing the ReadableStream cancellation 110 | try { 111 | const readableStream = new ReadableStream({ 112 | start(controller) { 113 | controller.enqueue("X"); 114 | controller.enqueue("Y"); 115 | }, 116 | cancel(reason) { 117 | assertEquals( 118 | reason, 119 | "Stream canceled", 120 | `Stream cancellation reason mismatch. Expected 'Stream canceled' but got ${reason}` 121 | ); 122 | }, 123 | }); 124 | 125 | const reader = readableStream.getReader(); 126 | await reader.cancel("Stream canceled"); 127 | 128 | const result = await reader.read(); 129 | const resultString = JSON.stringify(result); 130 | assertEquals( 131 | resultString, 132 | JSON.stringify({ done: true }), 133 | `Stream cancellation test failed. Expected { done: true } but got ${resultString}` 134 | ); 135 | } catch (error) { 136 | assert(false, `ReadableStream cancellation test failed: ${error}`); 137 | } 138 | 139 | // Testing the Error Propagation in ReadableStream 140 | try { 141 | const readableStream = new ReadableStream({ 142 | start(controller) { 143 | controller.enqueue("1"); 144 | controller.error(new Error("Stream error")); 145 | }, 146 | }); 147 | 148 | const transformStream = new TransformStream({ 149 | transform(chunk, controller) { 150 | controller.enqueue(chunk + " transformed"); 151 | }, 152 | }); 153 | 154 | const concatenatedErrors = []; 155 | try { 156 | const reader = readableStream.pipeThrough(transformStream).getReader(); 157 | while (true) { 158 | await reader.read(); 159 | } 160 | } catch (error) { 161 | concatenatedErrors.push(error.message); 162 | } 163 | 164 | assertEquals( 165 | concatenatedErrors[0], 166 | "Stream error", 167 | `Error propagation test failed. Expected 'Stream error' but got ${concatenatedErrors[0]}` 168 | ); 169 | } catch (error) { 170 | assert(false, `Stream error propagation test failed: ${error}`); 171 | } 172 | 173 | try { 174 | let stream = ReadableStream.from( 175 | (function* () { 176 | yield "a"; 177 | yield "b"; 178 | yield "c"; 179 | })() 180 | ); 181 | let arr = await readableStreamToArray(stream); 182 | assert_array_equals( 183 | arr, 184 | ["a", "b", "c"], 185 | "Stream contents should be correct" 186 | ); 187 | } catch (error) { 188 | assert(false, `ReadableStream.from test failed: ${error}`); 189 | } 190 | 191 | try { 192 | let stream = ReadableStream.from( 193 | (async function* () { 194 | yield "a"; 195 | yield "b"; 196 | yield "c"; 197 | })() 198 | ); 199 | let arr = await readableStreamToArray(stream); 200 | assert_array_equals( 201 | arr, 202 | ["a", "b", "c"], 203 | "Stream contents should be correct" 204 | ); 205 | } catch (error) { 206 | assert( 207 | false, 208 | `ReadableStream.from test with async iterable failed: ${error}` 209 | ); 210 | } 211 | 212 | // Create a response with the Blob's text 213 | return new Response("All tests passed!", { 214 | headers: { "content-type": "text/plain" }, 215 | }); 216 | } catch (error) { 217 | // If there's an error, return the error message in the response 218 | return new Response(error.message, { status: 500 }); 219 | } 220 | } 221 | 222 | export { handleRequest }; 223 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/12.2-text-encoder-stream.js: -------------------------------------------------------------------------------- 1 | import { assert_equals, promise_rejects_exactly, promise_test, readableStreamFromArray, readableStreamToArray, test } from "../test-utils"; 2 | 3 | async function handleRequest(request) { 4 | try { 5 | const error1 = new Error('error1'); 6 | error1.name = 'error1'; 7 | 8 | await promise_test(() => { 9 | const ts = new TextEncoderStream(); 10 | const writer = ts.writable.getWriter(); 11 | const reader = ts.readable.getReader(); 12 | const writePromise = writer.write({ 13 | toString() { throw error1; } 14 | }); 15 | const readPromise = reader.read(); 16 | return Promise.all([ 17 | promise_rejects_exactly(error1, readPromise, 'read should reject with error1'), 18 | promise_rejects_exactly(error1, writePromise, 'write should reject with error1'), 19 | promise_rejects_exactly(error1, reader.closed, 'readable should be errored with error1'), 20 | promise_rejects_exactly(error1, writer.closed, 'writable should be errored with error1'), 21 | ]); 22 | }, 'a chunk that cannot be converted to a string should error the streams'); 23 | 24 | const oddInputs = [ 25 | { 26 | name: 'string', 27 | value: 'hello!', 28 | expected: 'hello!' 29 | }, 30 | { 31 | name: 'undefined', 32 | value: undefined, 33 | expected: 'undefined' 34 | }, 35 | { 36 | name: 'null', 37 | value: null, 38 | expected: 'null' 39 | }, 40 | { 41 | name: 'numeric', 42 | value: 3.14, 43 | expected: '3.14' 44 | }, 45 | { 46 | name: 'object', 47 | value: {}, 48 | expected: '[object Object]' 49 | }, 50 | { 51 | name: 'array', 52 | value: ['hi'], 53 | expected: 'hi' 54 | } 55 | ]; 56 | 57 | for (const input of oddInputs) { 58 | await promise_test(async () => { 59 | const outputReadable = readableStreamFromArray([input.value]) 60 | .pipeThrough(new TextEncoderStream()) 61 | .pipeThrough(new TextDecoderStream()); 62 | const output = await readableStreamToArray(outputReadable); 63 | assert_equals(output.length, 1, 'output should contain one chunk'); 64 | assert_equals(output[0], input.expected, 'output should be correct'); 65 | }, `input of type ${input.name} should be converted correctly to string`); 66 | } 67 | 68 | test(() => { 69 | const te = new TextEncoderStream(); 70 | assert_equals(typeof ReadableStream.prototype.getReader.call(te.readable), 71 | 'object', 'readable property must pass brand check'); 72 | assert_equals(typeof WritableStream.prototype.getWriter.call(te.writable), 73 | 'object', 'writable property must pass brand check'); 74 | }, 'TextEncoderStream readable and writable properties must pass brand checks'); 75 | 76 | return new Response("All tests passed!", { 77 | headers: { "content-type": "text/plain" }, 78 | }); 79 | } catch (error) { 80 | // If there's an error, return the error message in the response 81 | return new Response(error.message, { status: 500 }); 82 | } 83 | } 84 | 85 | export { handleRequest }; -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/13-performance.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | try { 3 | let origin = performance.timeOrigin; 4 | if (origin === undefined) { 5 | throw new Error('Expected performance.timeOrigin to be defined'); 6 | } 7 | 8 | // We already have to use setTimeout, so let's test setInterval here as well 9 | 10 | let intervalsElapsed = 0; 11 | let intervalHandle = setInterval(() => intervalsElapsed += 1, 100); 12 | 13 | let now = performance.now(); 14 | await sleep(1000); 15 | let after1Sec = performance.now(); 16 | let elapsed = after1Sec - now; 17 | if (elapsed < 500 || elapsed > 3000) { 18 | throw new Error(`Expected elapsed time to be almost 1 second, but it's ${elapsed}MS`); 19 | } 20 | 21 | clearInterval(intervalHandle); 22 | 23 | let totalIntervals = intervalsElapsed; 24 | if (intervalsElapsed < 5 || intervalsElapsed > 30) { 25 | throw new Error(`Expected elapsed intervals to be almost 10, but it's ${intervalsElapsed}`); 26 | } 27 | 28 | await sleep(500); 29 | 30 | if (totalIntervals !== intervalsElapsed) { 31 | throw new Error(`More intervals elapsed after clearInterval was called, total before clearing ${totalIntervals}, after ${intervalsElapsed}`); 32 | } 33 | 34 | return new Response('All tests passed!'); 35 | } 36 | catch (e) { 37 | return new Response(e.toString(), { status: 500 }); 38 | } 39 | } 40 | 41 | const sleep = (n) => new Promise(resolve => setTimeout(resolve, n)); 42 | 43 | export { handleRequest }; -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/15-timers.js: -------------------------------------------------------------------------------- 1 | import { 2 | assert_unreached, 3 | async_test 4 | } from "../test-utils.js"; 5 | 6 | async function handleRequest(request) { 7 | try { 8 | await Promise.all([ 9 | async_test((t) => { 10 | let wasPreviouslyCalled = false; 11 | 12 | const handle = setInterval( 13 | t.step_func(() => { 14 | if (!wasPreviouslyCalled) { 15 | wasPreviouslyCalled = true; 16 | 17 | clearInterval(handle); 18 | 19 | // Make the test succeed after the callback would've run next. 20 | setTimeout(t.done, 750); 21 | } else { 22 | assert_unreached(); 23 | } 24 | }), 25 | 500 26 | ); 27 | }, "Clearing an interval from the callback should still clear it."), 28 | 29 | async_test((t) => { 30 | const handle = setTimeout( 31 | t.step_func(() => { 32 | assert_unreached("Timeout was not canceled"); 33 | }), 34 | 0 35 | ); 36 | 37 | clearInterval(handle); 38 | 39 | setTimeout(() => { 40 | t.done(); 41 | }, 100); 42 | }, "Clear timeout with clearInterval"), 43 | 44 | async_test((t) => { 45 | const handle = setInterval( 46 | t.step_func(() => { 47 | assert_unreached("Interval was not canceled"); 48 | }), 49 | 0 50 | ); 51 | 52 | clearTimeout(handle); 53 | 54 | setTimeout(() => { 55 | t.done(); 56 | }, 100); 57 | }, "Clear interval with clearTimeout"), 58 | ]); 59 | 60 | function timeout_trampoline(t, timeout, message) { 61 | t.step_timeout(function () { 62 | // Yield in case we managed to be called before the second interval callback. 63 | t.step_timeout(function () { 64 | assert_unreached(message); 65 | }, timeout); 66 | }, timeout); 67 | } 68 | 69 | await async_test(function (t) { 70 | let ctr = 0; 71 | let h = setInterval(t.step_func(function () { 72 | if (++ctr == 2) { 73 | clearInterval(h); 74 | t.done(); 75 | return; 76 | } 77 | }) /* no interval */); 78 | 79 | timeout_trampoline(t, 100, "Expected setInterval callback to be called two times"); 80 | }, "Calling setInterval with no interval should be the same as if called with 0 interval"); 81 | 82 | await async_test(function (t) { 83 | let ctr = 0; 84 | let h = setInterval(t.step_func(function () { 85 | if (++ctr == 2) { 86 | clearInterval(h); 87 | t.done(); 88 | return; 89 | } 90 | }), undefined); 91 | 92 | timeout_trampoline(t, 100, "Expected setInterval callback to be called two times"); 93 | }, "Calling setInterval with undefined interval should be the same as if called with 0 interval"); 94 | 95 | return new Response('All tests passed!'); 96 | } 97 | catch (e) { 98 | return new Response(e.toString(), { status: 500 }); 99 | } 100 | } 101 | 102 | export { handleRequest }; -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/16-crypto.js: -------------------------------------------------------------------------------- 1 | import { assert_equals, assert_throws_js, assert_true, test } from "../test-utils"; 2 | 3 | async function handleRequest(request) { 4 | try { 5 | const iterations = 256; 6 | // Track all the UUIDs generated during test run, bail if we ever collide: 7 | const uuids = new Set() 8 | function randomUUID() { 9 | const uuid = crypto.randomUUID(); 10 | if (uuids.has(uuid)) { 11 | throw new Error(`uuid collision ${uuid}`) 12 | } 13 | uuids.add(uuid); 14 | return uuid; 15 | } 16 | 17 | // UUID is in namespace format (16 bytes separated by dashes): 18 | test(function () { 19 | const UUIDRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/ 20 | for (let i = 0; i < iterations; i++) { 21 | assert_true(UUIDRegex.test(randomUUID())); 22 | } 23 | }, "namespace format"); 24 | 25 | // Set the 4 most significant bits of array[6], which represent the UUID 26 | // version, to 0b0100: 27 | test(function () { 28 | for (let i = 0; i < iterations; i++) { 29 | let value = parseInt(randomUUID().split('-')[2].slice(0, 2), 16); 30 | value &= 0b11110000; 31 | assert_true(value === 0b01000000); 32 | } 33 | }, "version set"); 34 | 35 | // Set the 2 most significant bits of array[8], which represent the UUID 36 | // variant, to 0b10: 37 | test(function () { 38 | for (let i = 0; i < iterations; i++) { 39 | // Grab the byte representing array[8]: 40 | let value = parseInt(randomUUID().split('-')[3].slice(0, 2), 16); 41 | value &= 0b11000000 42 | assert_true(value === 0b10000000); 43 | } 44 | }, "variant set"); 45 | 46 | test(function () { 47 | assert_throws_js(function () { 48 | crypto.getRandomValues(new Float32Array(6)) 49 | }, "Float32Array") 50 | assert_throws_js(function () { 51 | crypto.getRandomValues(new Float64Array(6)) 52 | }, "Float64Array") 53 | 54 | assert_throws_js(function () { 55 | const len = 65536 / Float32Array.BYTES_PER_ELEMENT + 1; 56 | crypto.getRandomValues(new Float32Array(len)); 57 | }, "Float32Array (too long)") 58 | assert_throws_js(function () { 59 | const len = 65536 / Float64Array.BYTES_PER_ELEMENT + 1; 60 | crypto.getRandomValues(new Float64Array(len)) 61 | }, "Float64Array (too long)") 62 | }, "Float arrays"); 63 | 64 | test(function () { 65 | assert_throws_js(function () { 66 | crypto.getRandomValues(new DataView(new ArrayBuffer(6))) 67 | }, "DataView") 68 | 69 | assert_throws_js(function () { 70 | crypto.getRandomValues(new DataView(new ArrayBuffer(65536 + 1))) 71 | }, "DataView (too long)") 72 | }, "DataView"); 73 | 74 | const arrays = [ 75 | 'Int8Array', 76 | 'Int16Array', 77 | 'Int32Array', 78 | 'BigInt64Array', 79 | 'Uint8Array', 80 | 'Uint8ClampedArray', 81 | 'Uint16Array', 82 | 'Uint32Array', 83 | 'BigUint64Array', 84 | ]; 85 | 86 | for (const array of arrays) { 87 | const ctor = globalThis[array]; 88 | 89 | test(function () { 90 | assert_equals(crypto.getRandomValues(new ctor(8)).constructor, 91 | ctor, "crypto.getRandomValues(new " + array + "(8))") 92 | }, "Integer array: " + array); 93 | 94 | test(function () { 95 | const maxLength = 65536 / ctor.BYTES_PER_ELEMENT; 96 | assert_throws_js(function () { 97 | crypto.getRandomValues(new ctor(maxLength + 1)) 98 | }, "crypto.getRandomValues length over 65536") 99 | }, "Large length: " + array); 100 | 101 | test(function () { 102 | assert_true(crypto.getRandomValues(new ctor(0)).length == 0) 103 | }, "Null arrays: " + array); 104 | } 105 | 106 | return new Response('All tests passed!'); 107 | } 108 | catch (e) { 109 | return new Response(e.toString(), { status: 500 }); 110 | } 111 | } 112 | 113 | export { handleRequest }; -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/18-event.js: -------------------------------------------------------------------------------- 1 | import { assert_equals } from "../test-utils"; 2 | 3 | async function handleRequest(request) { 4 | try { 5 | const TEST_EVENT1 = "testEvent1"; 6 | const TEST_EVENT2 = "testEvent2"; 7 | 8 | const target = new EventTarget(); 9 | const testEvent1 = new Event(TEST_EVENT1); 10 | const testEvent2 = new Event(TEST_EVENT2); 11 | 12 | let testEvent1CalledCount = 0; 13 | const testEvent1CallBack = () => { 14 | testEvent1CalledCount++; 15 | }; 16 | 17 | let testEvent2CalledCount = 0; 18 | const testEvent2CallBack = () => { 19 | testEvent2CalledCount++; 20 | }; 21 | 22 | target.addEventListener(TEST_EVENT1, testEvent1CallBack); 23 | target.addEventListener(TEST_EVENT2, testEvent2CallBack); 24 | 25 | // Check if dispatch can be executed successfully 26 | target.dispatchEvent(testEvent1); 27 | assert_equals( 28 | testEvent1CalledCount, 29 | 1, 30 | "test event 1 callback was not called" 31 | ); 32 | 33 | // Check if dispatch isn't executed when other event target was executed 34 | assert_equals( 35 | testEvent2CalledCount, 36 | 0, 37 | "test event 2 callback was unexpectedly called" 38 | ); 39 | 40 | // Check if dispatch can be executed successfully 41 | target.dispatchEvent(testEvent2); 42 | assert_equals( 43 | testEvent2CalledCount, 44 | 1, 45 | "test event 2 callback was not called" 46 | ); 47 | 48 | // Check if dispatch isn't executed when eventListener was removed 49 | target.removeEventListener(TEST_EVENT1, testEvent1CallBack); 50 | target.dispatchEvent(testEvent1); 51 | assert_equals( 52 | testEvent1CalledCount, 53 | 1, 54 | "test event 1 callback was unexpectedly called" 55 | ); 56 | 57 | // Check if dispatch can be executed successfully when other eventListener was remove 58 | target.dispatchEvent(testEvent2); 59 | assert_equals( 60 | testEvent2CalledCount, 61 | 2, 62 | "test event 2 callback was not called" 63 | ); 64 | 65 | // Check if dispatch isn't executed when eventListener was removed 66 | target.removeEventListener(TEST_EVENT2, testEvent2CallBack); 67 | target.dispatchEvent(testEvent2); 68 | assert_equals( 69 | testEvent2CalledCount, 70 | 2, 71 | "test event 2 callback was unexpectedly called" 72 | ); 73 | 74 | return new Response("All tests passed!"); 75 | } catch (error) { 76 | return new Response(error.message, { status: 500 }); 77 | } 78 | } 79 | 80 | export { handleRequest }; 81 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/2-blob.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | try { 3 | // Create a new Blob with some text 4 | const blobParts = ["Hello,", " world!"]; 5 | const myBlob = new Blob(blobParts, { type: "text/plain" }); 6 | 7 | // Use the text() method to read the Blob's text 8 | const text = await myBlob.text(); 9 | 10 | // Create a response with the Blob's text 11 | return new Response(text, { 12 | headers: { 13 | "Content-Type": myBlob.type, 14 | "Content-Length": myBlob.size.toString(), 15 | }, 16 | }); 17 | } catch (error) { 18 | // If there's an error, return the error message in the response 19 | return new Response(error.message, { status: 500 }); 20 | } 21 | } 22 | 23 | export { handleRequest }; 24 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/2.1-file.js: -------------------------------------------------------------------------------- 1 | import { assert, assert_equals, readStream } from "../test-utils"; 2 | 3 | async function handleRequest(request) { 4 | try { 5 | let file = new File(['abc', 'def'], 'file.txt', { type: 'text/plain', lastModified: 123 }); 6 | assert_equals(await file.text(), 'abcdef'); 7 | assert_equals(file.lastModified, 123); 8 | assert_equals(file.name, 'file.txt'); 9 | assert_equals(file.type, 'text/plain'); 10 | 11 | let stream = file.stream(); 12 | assert(stream instanceof ReadableStream, 'File.stream() should return an instance of ReadableStream'); 13 | 14 | let sliced = file.slice(2, 4, 'application/json'); 15 | assert_equals(await sliced.text(), 'cd'); 16 | assert_equals(sliced.type, 'application/json'); 17 | 18 | stream = sliced.stream(); 19 | let read = await readStream(stream); 20 | assert_equals(read, 'cd'); 21 | 22 | return new Response("All tests passed!", { 23 | headers: { "content-type": "text/plain" }, 24 | }); 25 | } catch (error) { 26 | return new Response(error.message, { status: 500 }); 27 | } 28 | } 29 | 30 | export { handleRequest }; 31 | 32 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/3-headers.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | // Parse the URL of the request to determine the action 3 | const url = new URL(request.url); 4 | const path = url.pathname; 5 | 6 | // Create a new Headers object 7 | const headers = new Headers({ 8 | "Content-Type": "text/plain", 9 | "X-Custom-Header": "CustomValue", 10 | }); 11 | 12 | if (path.includes("/append")) { 13 | // Append a new header 14 | headers.append("X-Appended-Header", "AppendedValue"); 15 | return new Response("Header appended", { headers }); 16 | } 17 | if (path.includes("/delete")) { 18 | // Delete a header 19 | headers.delete("X-Custom-Header"); 20 | return new Response("Header deleted", { headers }); 21 | } 22 | if (path.includes("/get")) { 23 | // Get the value of a header 24 | const contentType = headers.get("Content-Type"); 25 | return new Response(`Content-Type is ${contentType}`, { headers }); 26 | } 27 | if (path.includes("/has")) { 28 | // Check if a header exists 29 | const hasContentType = headers.has("Content-Type"); 30 | return new Response(`Has Content-Type: ${hasContentType}`, { headers }); 31 | } 32 | if (path.includes("/set")) { 33 | // Set the value of a header 34 | headers.set("Content-Type", "text/html"); 35 | return new Response("Content-Type set to text/html", { headers }); 36 | } 37 | if (path.includes("/iterate")) { 38 | // Iterate over headers and collect them 39 | let headersList = ""; 40 | for (const [name, value] of headers) { 41 | headersList += `${name}: ${value}\n`; 42 | } 43 | return new Response(`Headers iterated:\n${headersList}`, { headers }); 44 | } 45 | // Return all headers as a default response 46 | let allHeaders = ""; 47 | for (const [name, value] of headers) { 48 | allHeaders += `${name}: ${value}\n`; 49 | } 50 | return new Response(`All Headers:\n${allHeaders}`, { headers }); 51 | } 52 | 53 | export { handleRequest }; 54 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/4-request.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | // Clone the request to ensure it's a new, mutable Request object 3 | let newRequest; 4 | try { 5 | newRequest = new Request(request); 6 | } catch (error) { 7 | let message = "Error while cloning the request\n"; 8 | message += error.message; 9 | return new Response(message, { status: 500 }); 10 | } 11 | 12 | try { 13 | // Modify the Request object as per the `RequestInit` dictionary 14 | newRequest = new Request(newRequest, { 15 | method: "POST", 16 | headers: new Headers({ "X-Test-Header": "TestValue" }), 17 | referrer: "no-referrer", 18 | mode: "cors", 19 | credentials: "omit", 20 | cache: "default", 21 | redirect: "follow", 22 | integrity: "", 23 | keepalive: false, 24 | signal: null, 25 | duplex: "half", 26 | priority: "high", 27 | }); 28 | } catch (error) { 29 | let message = "Error while modifying the request\n"; 30 | message += error.message; 31 | return new Response(message, { status: 500 }); 32 | } 33 | 34 | let responseDetails; 35 | try { 36 | // Construct a response containing details from the Request object 37 | responseDetails = { 38 | method: newRequest.method, 39 | headers: [...newRequest.headers].reduce((obj, [key, value]) => { 40 | obj[key] = value; 41 | return obj; 42 | }, {}), 43 | referrer: newRequest.referrer, 44 | referrerPolicy: newRequest.referrerPolicy, 45 | mode: newRequest.mode, 46 | credentials: newRequest.credentials, 47 | cache: newRequest.cache, 48 | redirect: newRequest.redirect, 49 | integrity: newRequest.integrity, 50 | keepalive: newRequest.keepalive, 51 | isReloadNavigation: newRequest.isReloadNavigation, 52 | isHistoryNavigation: newRequest.isHistoryNavigation, 53 | signal: newRequest.signal, 54 | duplex: newRequest.duplex, 55 | }; 56 | } catch (error) { 57 | let message = "Error while constructing the response\n"; 58 | message += error.message; 59 | return new Response(message, { status: 500 }); 60 | } 61 | 62 | // Return a JSON response with the Request details 63 | return new Response(JSON.stringify(responseDetails), { 64 | headers: { "Content-Type": "application/json" }, 65 | }); 66 | } 67 | 68 | export { handleRequest }; 69 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/5-response.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | const testUrl = "https://example.com"; 3 | const testData = { key: "value" }; 4 | const testHeaders = new Headers({ "X-Custom-Header": "Test" }); 5 | 6 | try { 7 | // Test the basic constructor and property accessors 8 | const basicResponse = new Response("body content", { 9 | status: 200, 10 | statusText: "OK", 11 | headers: testHeaders, 12 | }); 13 | if (basicResponse.status !== 200) throw new Error("Status should be 200"); 14 | if (basicResponse.statusText !== "OK") 15 | throw new Error('Status text should be "OK"'); 16 | if (basicResponse.headers.get("X-Custom-Header") !== "Test") 17 | throw new Error("Custom header should be set"); 18 | } catch (error) { 19 | let message = "Error while basic construction of response\n"; 20 | message += error.message; 21 | return new Response(message, { status: 500 }); 22 | } 23 | 24 | try { 25 | // Test the Response.error() static method 26 | const errorResponse = Response.error(); 27 | if (errorResponse.type !== "error") 28 | throw new Error('Response type should be "error"'); 29 | if (errorResponse.status !== 0) 30 | throw new Error("Status for error response should be 0"); 31 | } catch (error) { 32 | let message = "Error while testing error response\n"; 33 | message += error.message; 34 | return new Response(message, { status: 500 }); 35 | } 36 | 37 | try { 38 | // Test the Response.redirect() static method 39 | const redirectResponse = Response.redirect(testUrl, 301); 40 | if (redirectResponse.status !== 301) 41 | throw new Error("Redirect status should be 301"); 42 | if (redirectResponse.headers.get("Location") !== testUrl) 43 | throw new Error("Location header should match the test URL"); 44 | } catch (error) { 45 | let message = "Error while testing redirect response\n"; 46 | message += error.message; 47 | return new Response(message, { status: 500 }); 48 | } 49 | try { 50 | // Test the Response.json() static method 51 | const jsonResponse = Response.json(testData); 52 | const data = await jsonResponse.json(); 53 | if (JSON.stringify(data) !== JSON.stringify(testData)) 54 | throw new Error("Body data should match the test data"); 55 | } catch (error) { 56 | let message = "Error while testing JSON response\n"; 57 | message += error.message; 58 | return new Response(message, { status: 500 }); 59 | } 60 | 61 | // If all tests pass, send a success response 62 | return new Response("All tests passed", { 63 | headers: { "Content-Type": "text/plain" }, 64 | }); 65 | } 66 | 67 | export { handleRequest }; 68 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/6-text-encoder.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | try { 3 | // Test the TextEncoder constructor 4 | const encoder = new TextEncoder(); 5 | if (!encoder) { 6 | throw new Error("TextEncoder constructor does not create an object."); 7 | } 8 | 9 | if (encoder.encoding !== "utf-8") { 10 | throw new Error( 11 | `Failed: TextEncoder 'encoding' attribute is not 'utf-8', it is '${encoder.encoding}'.` 12 | ); 13 | } 14 | 15 | const text = "Hello, world!"; 16 | const encoded = encoder.encode(text); 17 | if (!(encoded instanceof Uint8Array)) { 18 | throw new Error( 19 | "Failed: TextEncoder 'encode' method does not return a Uint8Array." 20 | ); 21 | } 22 | 23 | const source = "Hello, world!"; 24 | let destination = new Uint8Array(source.length * 3); // Allocate more space than needed 25 | const result = encoder.encodeInto(source, destination); 26 | 27 | if (typeof result.read !== "number" || typeof result.written !== "number") { 28 | throw new Error( 29 | "Failed: TextEncoder 'encodeInto' method does not return the expected object." 30 | ); 31 | } 32 | 33 | destination = new Uint8Array(source.length); // Allocate just enough space 34 | const result2 = encoder.encodeInto(source, destination); 35 | 36 | if (result2.read !== source.length || result2.written !== source.length) { 37 | throw new Error( 38 | "Failed: TextEncoder 'encodeInto' method does not return the expected object." 39 | ); 40 | } 41 | 42 | return new Response("All tests passed!", { 43 | headers: { "content-type": "text/plain" }, 44 | }); 45 | } catch (error) { 46 | return new Response(error.message, { status: 500 }); 47 | } 48 | } 49 | 50 | export { handleRequest }; 51 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/7-text-decoder.js: -------------------------------------------------------------------------------- 1 | // Utility function to get chunks of data 2 | function* nextChunk(textSource) { 3 | let currentPosition = 0; 4 | const CHUNK_SIZE = 10; 5 | 6 | while (currentPosition < textSource.length) { 7 | const chunk = textSource.slice( 8 | currentPosition, 9 | currentPosition + CHUNK_SIZE 10 | ); 11 | currentPosition += CHUNK_SIZE; 12 | 13 | const encoder = new TextEncoder(); 14 | const encodedChunk = encoder.encode(chunk); 15 | 16 | yield encodedChunk; 17 | } 18 | } 19 | 20 | async function handleRequest(request) { 21 | try { 22 | // Test the TextDecoder constructor 23 | try { 24 | const _ = new TextDecoder("invalid-encoding"); 25 | return new Response( 26 | "Failed: The constructor should throw a RangeError for an invalid encoding.", 27 | { status: 500 } 28 | ); 29 | } catch (e) { 30 | if (!(e instanceof RangeError)) { 31 | throw new Error("Failed: The error thrown is not a RangeError."); 32 | } 33 | } 34 | 35 | try { 36 | const encoding = "utf-8"; 37 | let _ = new TextDecoder(encoding); 38 | } catch (error) { 39 | throw new Error( 40 | "Failed: The constructor should not throw an error for a valid encoding." 41 | ); 42 | } 43 | 44 | try { 45 | const encoding = "utf-8"; 46 | let decoder = new TextDecoder(encoding); 47 | 48 | let string = ""; 49 | let textSource = 50 | "This is the complete text from which we will take chunks."; 51 | // Create an instance of the generator 52 | const chunkGenerator = nextChunk(textSource); 53 | 54 | // Iterate over the generator 55 | for ( 56 | let result = chunkGenerator.next(); 57 | !result.done; 58 | result = chunkGenerator.next() 59 | ) { 60 | // Get the buffer from the result 61 | const buffer = result.value; 62 | string += decoder.decode(buffer, { stream: true }); 63 | } 64 | if (string !== textSource) { 65 | throw new Error( 66 | "Failed: The decoded string does not match the source string." 67 | ); 68 | } 69 | } catch (error) { 70 | throw new Error(`Failed: ${error.message}`); 71 | } 72 | 73 | // Test the Fatal error mode 74 | const invalidData = new Uint8Array([0xff, 0xff, 0xff]); // Invalid UTF-8 sequence 75 | try { 76 | const decoder = new TextDecoder("utf-8", { fatal: true }); 77 | decoder.decode(invalidData); 78 | return new Response( 79 | "Failed: Decoding should throw a TypeError in fatal error mode.", 80 | { status: 500 } 81 | ); 82 | } catch (e) { 83 | if (!(e instanceof TypeError)) { 84 | throw new Error("Failed: The error thrown is not a TypeError."); 85 | } 86 | } 87 | 88 | return new Response("All tests passed!", { 89 | headers: { "content-type": "text/plain" }, 90 | }); 91 | } catch (error) { 92 | return new Response(error.message, { status: 500 }); 93 | } 94 | } 95 | 96 | export { handleRequest }; 97 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/8-url.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | try { 3 | // Using the URL constructor without base URL 4 | const myURL = new URL("https://example.org:443/foo?bar=baz#qux"); 5 | 6 | // check if the URL is valid 7 | if (!myURL) { 8 | throw new Error("URL constructor does not create an object."); 9 | } 10 | 11 | if (myURL.protocol !== "https:") { 12 | throw new Error( 13 | `Failed: URL 'protocol' attribute is not 'https:', it is '${myURL.protocol}'.` 14 | ); 15 | } 16 | 17 | if (myURL.port !== '443') { 18 | throw new Error( 19 | `Failed: URL 'port' attribute is not '443', it is '${myURL.port}'.` 20 | ); 21 | } 22 | 23 | if (myURL.host !== "example.org") { 24 | throw new Error( 25 | `Failed: URL 'host' attribute is not 'example.org', it is '${myURL.host}'.` 26 | ); 27 | } 28 | 29 | if (myURL.pathname !== "/foo") { 30 | throw new Error( 31 | `Failed: URL 'pathname' attribute is not '/foo', it is '${myURL.pathname}'.` 32 | ); 33 | } 34 | 35 | if (myURL.search !== "?bar=baz") { 36 | throw new Error( 37 | `Failed: URL 'search' attribute is not '?bar=baz', it is '${myURL.search}'.` 38 | ); 39 | } 40 | 41 | if (myURL.hash !== "#qux") { 42 | throw new Error( 43 | `Failed: URL 'hash' attribute is not '#qux', it is '${myURL.hash}'.` 44 | ); 45 | } 46 | 47 | // check if search params bar is baz in myURL 48 | if (myURL.searchParams.get("bar") !== "baz") { 49 | throw new Error( 50 | `Failed: URLSearchParams 'get' method does not return 'baz', it returns '${searchParams.get( 51 | "bar" 52 | )}'.` 53 | ); 54 | } 55 | 56 | // Try converting it to json 57 | 58 | try { 59 | const urlJSON = myURL.toJSON(); 60 | // check if urlJSON is valid 61 | if (!urlJSON) { 62 | throw new Error("URL 'toJSON' method does not return an object."); 63 | } 64 | } catch (error) { 65 | throw new Error( 66 | `Failed: URL 'toJSON' method does not return a json object.` 67 | ); 68 | } 69 | 70 | // Testing URL with a base 71 | try { 72 | const _ = new URL("/path", "https://example.com"); 73 | } catch (error) { 74 | throw new Error( 75 | `Failed: URL constructor does not create an object with a base.` 76 | ); 77 | } 78 | // Testing URL with a base 79 | try { 80 | const baseURL = new URL("https://example.com/base"); 81 | const _ = new URL("path", baseURL); 82 | } catch (error) { 83 | throw new Error( 84 | `Failed: URL constructor does not create an object with URL as its base.` 85 | ); 86 | } 87 | 88 | // Test URL with unicode characters 89 | 90 | try { 91 | // URL with unicode characters 92 | const _ = new URL("https://example.org/🔥"); 93 | } catch (error) { 94 | throw new Error( 95 | `Failed: URL constructor does not create an object with unicode characters.` 96 | ); 97 | } 98 | 99 | try { 100 | const _ = new URL("/path"); 101 | return new Response('Creating relative URL without a base succeeded when it should have failed', { status: 500 }); 102 | } catch (e) { 103 | } 104 | 105 | return new Response("All tests passed!", { 106 | headers: { "content-type": "text/plain" }, 107 | }); 108 | } catch (error) { 109 | return new Response(error.message, { status: 500 }); 110 | } 111 | } 112 | 113 | export { handleRequest }; 114 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-files/9-wait-until.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | try { 3 | // Create a response with the Blob's text 4 | return new Response("Hello World!", { 5 | headers: { "content-type": "text/plain" }, 6 | }); 7 | } catch (error) { 8 | // If there's an error, return the error message in the response 9 | return new Response(error.message, { status: 500 }); 10 | } 11 | } 12 | 13 | addEventListener("fetch", (event) => { 14 | event.respondWith( 15 | (async () => { 16 | // by definition, waitUntil is used to perform work *after* the handler 17 | // has returned, so we can't verify it's working since any verification 18 | // we do would be within the handler itself. We just check that it doesn't 19 | // throw an error here. 20 | event.waitUntil(handleRequest(event.request)); 21 | return handleRequest(event.request); 22 | })() 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /test-suite/js-test-app/src/test-utils.js: -------------------------------------------------------------------------------- 1 | const assert = (condition, message) => { 2 | if (!condition) { 3 | let msg = 4 | typeof message === "function" ? message() : message || "Assertion failed"; 5 | throw new Error(msg); 6 | } 7 | }; 8 | 9 | const assert_true = (condition, message) => { 10 | if (condition !== true) { 11 | throw new Error(message || "Assertion failed"); 12 | } 13 | }; 14 | 15 | const assert_false = (condition, message) => { 16 | if (condition !== false) { 17 | throw new Error(message || "Assertion failed"); 18 | } 19 | }; 20 | 21 | const assert_class_string = (obj, clsName, message) => { 22 | if (typeof obj !== "object") { 23 | throw new Error(`Expected ${obj} to an object: ${message}`); 24 | } 25 | 26 | if (obj.constructor.name !== clsName) { 27 | throw new Error(`Expected ${obj} to be of type ${clsName}: ${message}`); 28 | } 29 | }; 30 | 31 | const assert_array_equals = (array1, array2, message) => { 32 | if (array1.length != array2.length || array1.length === undefined) { 33 | throw new Error(`Expected ${array1} to be equal to ${array2}: ${message}`); 34 | } 35 | 36 | for (let i in array1) { 37 | if (array1[i] != array2[i]) { 38 | throw new Error( 39 | `Expected ${array1} to be equal to ${array2}: ${message}` 40 | ); 41 | } 42 | } 43 | 44 | // Make sure array2 has no keys that array1 doesn't 45 | for (let i in array2) { 46 | if (array1[i] != array2[i]) { 47 | throw new Error( 48 | `Expected ${array1} to be equal to ${array2}: ${message}` 49 | ); 50 | } 51 | } 52 | }; 53 | 54 | const assert_unreached = (message) => { 55 | throw new Error(message || "Assertion failed: should not be reached"); 56 | }; 57 | 58 | const assert_throws_js = (f, message) => { 59 | try { 60 | f(); 61 | throw undefined; 62 | } catch (e) { 63 | if (e === undefined) { 64 | throw new Error(`Should have thrown error: ${message}`); 65 | } 66 | } 67 | }; 68 | 69 | const assert_equals = (actual, expected, message) => { 70 | assert( 71 | actual === expected, 72 | () => `Expected ${expected} but got ${actual}: ${message}` 73 | ); 74 | }; 75 | 76 | const assert_not_equals = (actual, expected, message) => { 77 | assert( 78 | actual !== expected, 79 | () => `Expected ${expected} to be unequal to ${actual}: ${message}` 80 | ); 81 | }; 82 | 83 | const assert_less_than = (v1, v2, message) => { 84 | assert( 85 | v1 < v2, 86 | message || `Expected ${v1} to be greater than or equal to ${v1}` 87 | ); 88 | }; 89 | 90 | const assert_less_than_equal = (v1, v2, message) => { 91 | assert( 92 | v1 <= v2, 93 | message || `Expected ${v1} to be greater than or equal to ${v1}` 94 | ); 95 | }; 96 | 97 | const assert_greater_than = (v1, v2, message) => { 98 | assert( 99 | v1 > v2, 100 | message || `Expected ${v1} to be greater than or equal to ${v1}` 101 | ); 102 | }; 103 | 104 | const assert_greater_than_equal = (v1, v2, message) => { 105 | assert( 106 | v1 >= v2, 107 | message || `Expected ${v1} to be greater than or equal to ${v1}` 108 | ); 109 | }; 110 | 111 | const test = (f, desc) => { 112 | try { 113 | f({ 114 | unreached_func: (msg) => () => assert_unreached(msg), 115 | }); 116 | } catch (e) { 117 | throw new Error(`Test ${desc} failed with ${e}`); 118 | } 119 | }; 120 | 121 | const promise_test = async (f, desc) => { 122 | try { 123 | await f({ 124 | unreached_func: (msg) => () => assert_unreached(msg), 125 | }); 126 | } catch (e) { 127 | throw new Error(`Test ${desc} failed with ${e}`); 128 | } 129 | }; 130 | 131 | const promise_rejects_js = (p, message) => { 132 | return p.then( 133 | (result) => 134 | assert_unreached( 135 | `Promise should throw but succeeded with ${result}: ${message}` 136 | ), 137 | (_) => {} 138 | ); 139 | }; 140 | 141 | const promise_rejects_exactly = (error, p, message) => { 142 | return p.then( 143 | (result) => 144 | assert_unreached( 145 | `Promise should throw but succeeded with ${result}: ${message}` 146 | ), 147 | (e) => assert_equals(e, error, message) 148 | ); 149 | }; 150 | 151 | const readStream = async (stream) => { 152 | let reader = stream.pipeThrough(new TextDecoderStream()).getReader(); 153 | let result = ""; 154 | while (true) { 155 | let chunk = await reader.read(); 156 | if (chunk.done) { 157 | break; 158 | } 159 | result += chunk.value.toString(); 160 | } 161 | return result; 162 | }; 163 | 164 | const readableStreamFromArray = (arr) => { 165 | let s = new ReadableStream({ 166 | start: (controller) => { 167 | for (let a of arr) { 168 | controller.enqueue(a); 169 | } 170 | controller.close(); 171 | }, 172 | }); 173 | return s; 174 | }; 175 | 176 | const readableStreamToArray = async (stream) => { 177 | let reader = stream.getReader(); 178 | let result = []; 179 | while (true) { 180 | let chunk = await reader.read(); 181 | if (chunk.done) { 182 | break; 183 | } 184 | result.push(chunk.value); 185 | } 186 | return result; 187 | }; 188 | 189 | const async_test = (f, n) => { 190 | // console.log(`Starting test ${n}`); 191 | let resolve, reject; 192 | let done = false; 193 | 194 | let p = new Promise((res, rej) => { 195 | resolve = () => { 196 | // console.log(`Test ${n} succeeded`); 197 | done = true; 198 | res(); 199 | }; 200 | reject = (e) => { 201 | // console.log(`Test ${n} failed with ${e}`); 202 | done = true; 203 | rej(e); 204 | }; 205 | }); 206 | 207 | let t = { 208 | step_func: (f) => { 209 | if (!done) { 210 | return () => { 211 | try { 212 | f(); 213 | } catch (e) { 214 | reject(e); 215 | } 216 | }; 217 | } else { 218 | return () => {}; 219 | } 220 | }, 221 | step_timeout: (f, t) => { 222 | if (!done) { 223 | setTimeout(() => { 224 | try { 225 | f(); 226 | } catch (e) { 227 | reject(e); 228 | } 229 | }, t); 230 | } 231 | }, 232 | done: () => { 233 | if (!done) { 234 | resolve(); 235 | } 236 | }, 237 | }; 238 | 239 | f(t); 240 | 241 | return p; 242 | }; 243 | 244 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 245 | 246 | const flushAsyncEvents = () => 247 | delay(1) 248 | .then(() => delay(0)) 249 | .then(() => delay(0)) 250 | .then(() => delay(0)); 251 | 252 | export { 253 | assert, 254 | assert_class_string, 255 | assert_array_equals, 256 | assert_equals, 257 | assert_not_equals, 258 | assert_greater_than, 259 | assert_greater_than_equal, 260 | assert_less_than, 261 | assert_less_than_equal, 262 | assert_false, 263 | assert_throws_js, 264 | assert_true, 265 | assert_unreached, 266 | delay, 267 | flushAsyncEvents, 268 | promise_test, 269 | promise_rejects_js, 270 | promise_rejects_exactly, 271 | test, 272 | readStream, 273 | readableStreamFromArray, 274 | readableStreamToArray, 275 | async_test, 276 | }; 277 | -------------------------------------------------------------------------------- /test-suite/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{bail, Result}; 4 | use futures::{stream::FuturesUnordered, StreamExt}; 5 | use libtest_mimic::{Arguments, Failed, Trial}; 6 | use reqwest::StatusCode; 7 | use serde::Deserialize; 8 | 9 | #[derive(Debug, Clone, Deserialize)] 10 | pub struct TestCase { 11 | pub test_name: String, 12 | pub test_route: String, 13 | pub expected_output: String, 14 | pub expected_response_status: u16, 15 | 16 | // Timeout in seconds, will be ignored if zero 17 | pub timeout: Option, 18 | 19 | // We don't do anything with the string, but it lets us have 20 | // documentation in the config file as to why we're skipping 21 | pub skip: Option, 22 | } 23 | 24 | #[derive(Debug, Clone, Deserialize)] 25 | pub struct TestConfig { 26 | #[serde(rename = "test_case")] 27 | pub test_cases: Vec, 28 | } 29 | 30 | #[derive(Clone, Debug)] 31 | pub struct ServerConfig { 32 | pub host: String, 33 | pub host_header: Option, 34 | pub port: u16, 35 | pub critical: bool, 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | pub struct StressTestConfig { 40 | pub duration: Duration, 41 | pub max_concurrent_connections: usize, 42 | pub min_connections: usize, 43 | pub max_connections: Option, 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct TestManager { 48 | rt: tokio::runtime::Runtime, 49 | test_config: TestConfig, 50 | server_config: ServerConfig, 51 | stress_config: Option, 52 | } 53 | 54 | impl TestManager { 55 | pub fn new( 56 | test_config: TestConfig, 57 | server_config: ServerConfig, 58 | stress_config: Option, 59 | ) -> Result { 60 | let rt = tokio::runtime::Runtime::new()?; 61 | Ok(TestManager { 62 | rt, 63 | test_config, 64 | server_config, 65 | stress_config, 66 | }) 67 | } 68 | 69 | fn collect_tests(&self) -> Result, anyhow::Error> { 70 | let mut tests = vec![]; 71 | 72 | for test_case in self.test_config.test_cases.iter() { 73 | let test_name = test_case.test_name.clone(); 74 | 75 | if test_case.skip.is_some() { 76 | println!("Skipping {test_name}"); 77 | continue; 78 | } 79 | 80 | let rt = self.rt.handle().clone(); 81 | let test_case = test_case.clone(); 82 | 83 | let server_config = self.server_config.clone(); 84 | let test = match self.stress_config { 85 | Some(ref stress_config) => { 86 | let stress_config = stress_config.clone(); 87 | Trial::test(test_name, move || { 88 | stress_test(rt, server_config, stress_config, test_case) 89 | }) 90 | } 91 | None => Trial::test(test_name, move || test_once(rt, server_config, test_case)), 92 | }; 93 | 94 | tests.push(test); 95 | } 96 | 97 | Ok(tests) 98 | } 99 | 100 | pub fn run_tests(self, mut args: Arguments) -> Result<(), anyhow::Error> { 101 | let tests = self.collect_tests()?; 102 | if self.stress_config.is_some() { 103 | // Use a single thread for stress tests, since the many concurrent 104 | // connections can exhaust available ports and cause failures 105 | args.test_threads = Some(1); 106 | } 107 | let conclusion = libtest_mimic::run(&args, tests); 108 | if conclusion.has_failed() { 109 | if self.server_config.critical { 110 | bail!("Some tests failed") 111 | } else { 112 | println!( 113 | "Warning: Some tests failed, check test output above to \ 114 | make sure the test cases are correct" 115 | ); 116 | } 117 | } 118 | 119 | Ok(()) 120 | } 121 | } 122 | 123 | fn stress_test( 124 | rt: tokio::runtime::Handle, 125 | server_config: ServerConfig, 126 | stress_config: StressTestConfig, 127 | test_case: TestCase, 128 | ) -> Result<(), Failed> { 129 | rt.block_on(async move { 130 | let client = reqwest::ClientBuilder::new() 131 | .pool_max_idle_per_host(100) 132 | .build()?; 133 | 134 | let mut futures = FuturesUnordered::new(); 135 | 136 | let mut error = None; 137 | let mut connections = 0; 138 | 139 | let start_instant = std::time::Instant::now(); 140 | 141 | loop { 142 | if start_instant.elapsed() >= stress_config.duration 143 | && connections >= stress_config.min_connections 144 | { 145 | break; 146 | } 147 | 148 | if let Some(max_connections) = stress_config.max_connections { 149 | if connections >= max_connections { 150 | break; 151 | } 152 | } 153 | 154 | while futures.len() < stress_config.max_concurrent_connections { 155 | futures.push(run_test_once(&server_config, &test_case, &client)); 156 | connections += 1; 157 | } 158 | 159 | let res = futures.next().await; 160 | if let Some(Err(e)) = res { 161 | error = Some(e.into()); 162 | break; 163 | } 164 | } 165 | 166 | if error.is_none() { 167 | while !futures.is_empty() { 168 | let res = futures.next().await; 169 | if let Some(Err(e)) = res { 170 | error = Some(e.into()); 171 | break; 172 | } 173 | } 174 | } 175 | 176 | match error { 177 | None => { 178 | println!( 179 | "Test {} - {connections} successful connections", 180 | test_case.test_name 181 | ); 182 | Ok(()) 183 | } 184 | Some(e) => { 185 | println!( 186 | "Test {} - error after {connections} connections", 187 | test_case.test_name 188 | ); 189 | Err(e) 190 | } 191 | } 192 | }) 193 | } 194 | 195 | fn test_once( 196 | rt: tokio::runtime::Handle, 197 | server_config: ServerConfig, 198 | test_case: TestCase, 199 | ) -> Result<(), Failed> { 200 | let client = reqwest::Client::new(); 201 | rt.block_on(run_test_once(&server_config, &test_case, &client))?; 202 | Ok(()) 203 | } 204 | 205 | async fn run_test_once( 206 | server_config: &ServerConfig, 207 | test_case: &TestCase, 208 | client: &reqwest::Client, 209 | ) -> Result<()> { 210 | let expected_response_status = 211 | StatusCode::from_u16(test_case.expected_response_status).expect("Invalid status code"); 212 | 213 | let url = format!( 214 | "http://{}:{}/{}", 215 | server_config.host, server_config.port, test_case.test_route 216 | ); 217 | let mut request = client.get(&url); 218 | if let Some(host_header) = server_config.host_header.as_ref() { 219 | request = request.header("Host", host_header); 220 | } 221 | if let Some(timeout) = test_case.timeout { 222 | request = request.timeout(std::time::Duration::from_secs_f64(timeout)); 223 | } 224 | 225 | let response = request.send().await?; 226 | let response_status = response.status(); 227 | let response_body = response.text().await?; 228 | 229 | if response_body != test_case.expected_output { 230 | bail!( 231 | "Response body '{response_body}' doesn't match expected body '{}'", 232 | test_case.expected_output 233 | ); 234 | } 235 | 236 | if response_status != expected_response_status { 237 | bail!("Response status {response_status} doesn't match expected status {expected_response_status}"); 238 | } 239 | 240 | anyhow::Ok(()) 241 | } 242 | -------------------------------------------------------------------------------- /test-suite/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use test_suite::{ServerConfig, StressTestConfig, TestConfig, TestManager}; 4 | 5 | use std::{fs, path::Path, time::Duration}; 6 | 7 | fn read_test_cases(test_definition_file_path: Option>) -> Result { 8 | let file_content = fs::read_to_string( 9 | test_definition_file_path 10 | .as_ref() 11 | .map(|p| p.as_ref()) 12 | .unwrap_or("winterjs-tests.toml".as_ref()), 13 | ) 14 | .expect("Cannot read test definition file"); 15 | let test_config: TestConfig = toml::from_str(file_content.as_str())?; 16 | Ok(test_config) 17 | } 18 | 19 | #[derive(clap::Parser)] 20 | struct TestSuiteArguments { 21 | /// Path to the test definition file. Defaults to './winterjs-tests.toml' 22 | #[arg(short = 'c', long)] 23 | test_config_path: Option, 24 | 25 | /// Defaults to localhost 26 | #[arg(long)] 27 | host: Option, 28 | 29 | /// If set, send Host header with requests 30 | #[arg(long)] 31 | host_header: Option, 32 | 33 | /// Defaults to 8080 34 | #[arg(long)] 35 | port: Option, 36 | 37 | /// If specified, exit code will be 0 regardless of whether tests succeed. 38 | /// Useful for running test suite against other engines 39 | #[arg(long)] 40 | not_critical: bool, 41 | 42 | /// The number of seconds to run a stress test for. If left out, each test 43 | /// will be run once only. 44 | #[arg(long)] 45 | stress_duration: Option, 46 | 47 | /// Maximum number of concurrent connections when stress testing. 48 | /// Defaults to 100. 49 | #[arg(long)] 50 | stress_max_concurrent_connections: Option, 51 | 52 | /// The minimum number of connections that have to be made 53 | /// for the stress test to be considered successful. The test 54 | /// will not be stopped if this number of connections haven't 55 | /// been made once the duration is up. 56 | #[arg(long)] 57 | stress_min_connections: Option, 58 | 59 | /// The maximum number of connections that can be made. The test 60 | /// will be considered successful if this many connections are 61 | /// made regardless of whether the duration is up. 62 | #[arg(long)] 63 | stress_max_connections: Option, 64 | 65 | #[clap(flatten)] 66 | test_args: libtest_mimic::Arguments, 67 | } 68 | 69 | fn main() -> Result<()> { 70 | let args = TestSuiteArguments::parse(); 71 | 72 | let test_config = read_test_cases(args.test_config_path)?; 73 | let server_config = ServerConfig { 74 | host: args.host.unwrap_or_else(|| "localhost".to_string()), 75 | host_header: args.host_header, 76 | port: args.port.unwrap_or(8080), 77 | critical: !args.not_critical, 78 | }; 79 | let stress_config = args.stress_duration.map(|d| StressTestConfig { 80 | duration: Duration::from_secs(d as u64), 81 | max_concurrent_connections: args.stress_max_concurrent_connections.unwrap_or(100), 82 | min_connections: args.stress_min_connections.unwrap_or(0), 83 | max_connections: args.stress_max_connections, 84 | }); 85 | 86 | TestManager::new(test_config, server_config, stress_config)?.run_tests(args.test_args) 87 | } 88 | -------------------------------------------------------------------------------- /test-suite/winterjs-tests.toml: -------------------------------------------------------------------------------- 1 | [[test_case]] 2 | test_name = "1-hello" 3 | test_route = "1-hello" 4 | expected_output = "hello" 5 | expected_response_status = 200 6 | 7 | [[test_case]] 8 | test_name = "2-blob" 9 | test_route = "2-blob" 10 | expected_output = "Hello, world!" 11 | expected_response_status = 200 12 | 13 | [[test_case]] 14 | test_name = "2.1-file" 15 | test_route = "2.1-file" 16 | expected_output = "All tests passed!" 17 | expected_response_status = 200 18 | 19 | [[test_case]] 20 | test_name = "3.1-headers" 21 | test_route = "3-headers" 22 | expected_output = "All Headers:\ncontent-type: text/plain\nx-custom-header: CustomValue\n" 23 | expected_response_status = 200 24 | 25 | [[test_case]] 26 | test_name = "3.2-headers" 27 | test_route = "3-headers/append" 28 | expected_output = "Header appended" 29 | expected_response_status = 200 30 | 31 | [[test_case]] 32 | test_name = "3.3-headers" 33 | test_route = "3-headers/delete" 34 | expected_output = "Header deleted" 35 | expected_response_status = 200 36 | 37 | [[test_case]] 38 | test_name = "3.4-headers" 39 | test_route = "3-headers/get" 40 | expected_output = "Content-Type is text/plain" 41 | expected_response_status = 200 42 | 43 | [[test_case]] 44 | test_name = "3.5-headers" 45 | test_route = "3-headers/set" 46 | expected_output = "Content-Type set to text/html" 47 | expected_response_status = 200 48 | 49 | [[test_case]] 50 | test_name = "3.6-headers" 51 | test_route = "3-headers/has" 52 | expected_output = "Has Content-Type: true" 53 | expected_response_status = 200 54 | 55 | [[test_case]] 56 | test_name = "3.7-headers" 57 | test_route = "3-headers/iterate" 58 | expected_output = "Headers iterated:\ncontent-type: text/plain\nx-custom-header: CustomValue\n" 59 | expected_response_status = 200 60 | 61 | [[test_case]] 62 | test_name = "4-request" 63 | test_route = "4-request" 64 | expected_output = "{\"method\":\"POST\",\"headers\":{\"x-test-header\":\"TestValue\"},\"referrer\":\"no-referrer\",\"referrerPolicy\":\"no-referrer\",\"mode\":\"cors\",\"credentials\":\"omit\",\"cache\":\"default\",\"redirect\":\"follow\",\"integrity\":\"\",\"keepalive\":false,\"isReloadNavigation\":false,\"isHistoryNavigation\":false,\"signal\":{},\"duplex\":\"half\"}" 65 | expected_response_status = 200 66 | 67 | [[test_case]] 68 | test_name = "5-response" 69 | test_route = "5-response" 70 | expected_output = "All tests passed" 71 | expected_response_status = 200 72 | 73 | [[test_case]] 74 | test_name = "6-text-encoder" 75 | test_route = "6-text-encoder" 76 | expected_output = "All tests passed!" 77 | expected_response_status = 200 78 | 79 | [[test_case]] 80 | test_name = "7-text-decoder" 81 | test_route = "7-text-decoder" 82 | expected_output = "All tests passed!" 83 | expected_response_status = 200 84 | 85 | [[test_case]] 86 | test_name = "8-url" 87 | test_route = "8-url" 88 | expected_output = "All tests passed!" 89 | expected_response_status = 200 90 | 91 | [[test_case]] 92 | test_name = "8.1-search-params" 93 | test_route = "8.1-search-params" 94 | expected_output = "All tests passed!" 95 | expected_response_status = 200 96 | 97 | [[test_case]] 98 | test_name = "10-atob-btoa" 99 | test_route = "10-atob-btoa" 100 | expected_output = "All tests passed!" 101 | expected_response_status = 200 102 | 103 | [[test_case]] 104 | test_name = "11-fetch" 105 | test_route = "11-fetch" 106 | expected_output = "All tests passed!" 107 | expected_response_status = 200 108 | 109 | [[test_case]] 110 | test_name = "11.1-fetch-body" 111 | test_route = "11.1-fetch-body" 112 | expected_output = "All tests passed!" 113 | expected_response_status = 200 114 | 115 | [[test_case]] 116 | test_name = "12-streams" 117 | test_route = "12-streams" 118 | expected_output = "All tests passed!" 119 | expected_response_status = 200 120 | 121 | [[test_case]] 122 | test_name = "12.1-transform-stream" 123 | test_route = "12.1-transform-stream" 124 | expected_output = "All tests passed!" 125 | expected_response_status = 200 126 | 127 | [[test_case]] 128 | test_name = "12.2-text-encoder-stream" 129 | test_route = "12.2-text-encoder-stream" 130 | expected_output = "All tests passed!" 131 | expected_response_status = 200 132 | 133 | [[test_case]] 134 | test_name = "12.3-text-decoder-stream" 135 | test_route = "12.3-text-decoder-stream" 136 | expected_output = "All tests passed!" 137 | expected_response_status = 200 138 | 139 | [[test_case]] 140 | test_name = "13-performance" 141 | test_route = "13-performance" 142 | expected_output = "All tests passed!" 143 | expected_response_status = 200 144 | 145 | [[test_case]] 146 | test_name = "14-form-data" 147 | test_route = "14-form-data" 148 | expected_output = "All tests passed!" 149 | expected_response_status = 200 150 | 151 | [[test_case]] 152 | test_name = "15-timers" 153 | test_route = "15-timers" 154 | expected_output = "All tests passed!" 155 | expected_response_status = 200 156 | 157 | [[test_case]] 158 | test_name = "16-crypto" 159 | test_route = "16-crypto" 160 | expected_output = "All tests passed!" 161 | expected_response_status = 200 162 | 163 | [[test_case]] 164 | test_name = "16.1-crypto-hmac" 165 | test_route = "16.1-crypto-hmac" 166 | expected_output = "All tests passed!" 167 | expected_response_status = 200 168 | 169 | [[test_case]] 170 | test_name = "16.2-crypto-sha" 171 | test_route = "16.2-crypto-sha" 172 | expected_output = "All tests passed!" 173 | expected_response_status = 200 174 | 175 | [[test_case]] 176 | test_name = "17-cache" 177 | test_route = "17-cache" 178 | expected_output = "All tests passed!" 179 | expected_response_status = 200 180 | 181 | [[test_case]] 182 | test_name = "18-event" 183 | test_route = "18-event" 184 | expected_output = "All tests passed!" 185 | expected_response_status = 200 -------------------------------------------------------------------------------- /tests/edge-template.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(request) { 2 | const out = JSON.stringify({ 3 | success: true, 4 | package: "owner/package-name", 5 | }); 6 | return new Response(out, { 7 | headers: { "content-type": "application/json" }, 8 | }); 9 | } 10 | 11 | addEventListener("fetch", e => { 12 | e.respondWith(handleRequest(e.request)); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/fetch.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', req => { 2 | req.respondWith(handleRequest(req)); 3 | }); 4 | 5 | async function handleRequest(req) { 6 | let url = await req.request.text(); 7 | console.log("Fetching", url); 8 | let h = req.request.headers.get('x-wasmer-test'); 9 | console.log("Header val:", h); 10 | let res = await fetch(url); 11 | let text = await res.text(); 12 | return new Response(text); 13 | } -------------------------------------------------------------------------------- /tests/promise.js: -------------------------------------------------------------------------------- 1 | 2 | async function handleRequest(req) { 3 | await sleep(1000); 4 | return new Response('hello'); 5 | } 6 | 7 | const sleep = n => new Promise(resolve => setTimeout(resolve, n)); 8 | 9 | addEventListener('fetch', req => { 10 | req.respondWith(handleRequest(req)); 11 | }); -------------------------------------------------------------------------------- /tests/simple.js: -------------------------------------------------------------------------------- 1 | async function handleRequest(req) { 2 | return new Response('hello'); 3 | } 4 | 5 | addEventListener('fetch', req => { 6 | req.respondWith(handleRequest(req)); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/wrangler.js: -------------------------------------------------------------------------------- 1 | // THIS FAILS: Investigate 2 | function assert(condition, message) { 3 | if (!condition) { 4 | throw new Error(message || "Assertion failed"); 5 | } 6 | } 7 | 8 | function assertEquals(actual, expected, message) { 9 | assert( 10 | actual === expected, 11 | message || `Expected ${expected} but got ${actual}` 12 | ); 13 | } 14 | 15 | async function handleRequest(request) { 16 | try { 17 | const string = "Hello, world!"; 18 | const base64Encoded = "SGVsbG8sIHdvcmxkIQ=="; 19 | 20 | // Test btoa 21 | const encoded = btoa(string); 22 | assertEquals( 23 | encoded, 24 | base64Encoded, 25 | "btoa did not encode the string correctly" 26 | ); 27 | 28 | // Test atob 29 | const decoded = atob(base64Encoded); 30 | assertEquals(decoded, string, "atob did not decode the string correctly"); 31 | 32 | // Test btoa with binary data 33 | try { 34 | const binaryData = "\x00\x01\x02"; 35 | btoa(binaryData); 36 | assert(true, "btoa handled binary data without throwing error"); 37 | } catch (e) { 38 | assert(false, "btoa should not throw error with binary data"); 39 | } 40 | 41 | // Test atob with invalid input 42 | try { 43 | atob("Invalid base64 string"); 44 | assert(false, "atob should throw error with invalid base64 input"); 45 | } catch (e) { 46 | assert(true, "atob threw error as expected with invalid base64 input"); 47 | } 48 | // Create a response with the Blob's text 49 | return new Response("All tests passed!", { 50 | headers: { "content-type": "text/plain" }, 51 | }); 52 | } catch (error) { 53 | // If there's an error, return the error message in the response 54 | return new Response(error.message, { status: 500 }); 55 | } 56 | } 57 | 58 | addEventListener("fetch", event => { 59 | event.respondWith(handleRequest(event.request)); 60 | }); -------------------------------------------------------------------------------- /wasmer.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'wasmer/winterjs' 3 | version = '1.1.5' 4 | description = 'The JavaScript runtime that brings JavaScript to Wasmer Edge.' 5 | license = 'MIT' 6 | readme = 'README.md' 7 | repository = 'https://github.com/wasmerio/winterjs/' 8 | entrypoint = 'winterjs' 9 | 10 | # See more keys and definitions at https://docs.wasmer.io/registry/manifest 11 | 12 | [dependencies] 13 | 14 | [[module]] 15 | name = 'winterjs' 16 | source = 'target/wasm32-wasmer-wasi/release/winterjs.wasm' 17 | abi = 'wasi' 18 | 19 | [[command]] 20 | name = 'winterjs' 21 | module = 'winterjs' 22 | 23 | [[command]] 24 | name = 'wasmer-winter' 25 | module = 'winterjs' 26 | --------------------------------------------------------------------------------