├── .cargo └── config.toml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── ci_install.yml │ └── publish.yml ├── .gitignore ├── .vscode └── ltex.dictionary.en-US.txt ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── crates ├── cli │ ├── Cargo.toml │ ├── build.rs │ ├── script.js │ └── src │ │ ├── main.rs │ │ ├── opt.rs │ │ ├── options.rs │ │ ├── shims.rs │ │ └── ts_parser.rs └── core │ ├── Cargo.toml │ ├── build.rs │ └── src │ ├── globals.rs │ ├── lib.rs │ └── prelude │ ├── .gitignore │ ├── README.md │ ├── esbuild.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── config.ts │ ├── console.ts │ ├── date.ts │ ├── host.ts │ ├── http.ts │ ├── index.ts │ ├── memory-handle.ts │ ├── memory.ts │ ├── text-decoder.ts │ ├── text-encoder.ts │ └── var.ts │ ├── tsconfig.json │ └── types │ ├── plugin-interface.d.ts │ └── polyfills.d.ts ├── example-schema.yaml ├── examples ├── base64 │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js ├── bundled │ ├── .gitignore │ ├── esbuild.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.d.ts │ │ └── index.ts │ └── tsconfig.json ├── console │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js ├── exception │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js ├── exports │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js ├── host_funcs │ ├── go.mod │ ├── go.sum │ ├── jsconfig.json │ ├── main.go │ ├── script.d.ts │ └── script.js ├── kitchen-sink │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── esbuild.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.d.ts │ │ └── index.ts │ └── tsconfig.json ├── react │ ├── .gitignore │ ├── esbuild.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.d.ts │ │ └── index.tsx │ └── tsconfig.json ├── simple_js │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js └── try-catch │ ├── jsconfig.json │ ├── script.d.ts │ └── script.js ├── install-wasi-sdk.ps1 ├── install-wasi-sdk.sh ├── install-windows.ps1 └── install.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bhelx @zshipko 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | include: 11 | - name: linux 12 | os: ubuntu-24.04 13 | # Re-enable once we can build on Windows again 14 | # - name: windows 15 | # os: windows-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: default 21 | toolchain: stable 22 | target: wasm32-wasip1 23 | default: true 24 | 25 | - name: Setup Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.x' 29 | 30 | - name: Setup Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.12' 34 | 35 | - name: Install Extism CLI 36 | run: | 37 | go install github.com/extism/cli/extism@v1.6.0 38 | extism --version 39 | 40 | - name: Update deps (Linux) 41 | run: | 42 | ./install-wasi-sdk.sh 43 | cd /tmp 44 | # get just wasm-merge and wasm-opt 45 | curl -L https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz > binaryen.tar.gz 46 | tar xvzf binaryen.tar.gz 47 | sudo cp binaryen-version_116/bin/wasm-merge /usr/local/bin 48 | sudo cp binaryen-version_116/bin/wasm-opt /usr/local/bin 49 | if: runner.os != 'Windows' 50 | 51 | - name: Update deps (Windows) 52 | run: | 53 | powershell -executionpolicy bypass -File .\install-wasi-sdk.ps1 54 | go install github.com/extism/cli/extism@latest 55 | Remove-Item -Recurse -Path "c:\Program files\Binaryen" -Force -ErrorAction SilentlyContinue > $null 2>&1 56 | New-Item -ItemType Directory -Force -Path "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 57 | Invoke-WebRequest -Uri "https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-windows.tar.gz" -OutFile "$env:TMP\binaryen-version_116-x86_64-windows.tar.gz" 58 | 7z x "$env:TMP\binaryen-version_116-x86_64-windows.tar.gz" -o"$env:TMP\" >$null 2>&1 59 | 7z x -ttar "$env:TMP\binaryen-version_116-x86_64-windows.tar" -o"$env:TMP\" >$null 2>&1 60 | Copy-Item -Path "$env:TMP\binaryen-version_116\bin\wasm-opt.exe" -Destination "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 61 | Copy-Item -Path "$env:TMP\binaryen-version_116\bin\wasm-merge.exe" -Destination "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 62 | if: runner.os == 'Windows' 63 | 64 | - name: Run Tests (Linux) 65 | env: 66 | QUICKJS_WASM_SYS_WASI_SDK_PATH: "${{ github.workspace }}/wasi-sdk" 67 | run: | 68 | make 69 | make test 70 | if: runner.os != 'Windows' 71 | 72 | - name: Run Tests (Windows) 73 | env: 74 | QUICKJS_WASM_SYS_WASI_SDK_PATH: "${{ github.workspace }}/wasi-sdk" 75 | run: | 76 | set PATH="c:\Program files\Binaryen\";%PATH% 77 | make 78 | make test 79 | shell: cmd 80 | if: runner.os == 'Windows' 81 | -------------------------------------------------------------------------------- /.github/workflows/ci_install.yml: -------------------------------------------------------------------------------- 1 | name: "CI Install Script" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | include: 11 | - name: linux 12 | os: ubuntu-24.04 13 | - name: windows 14 | os: windows-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Test Install Script (Linux) 19 | run: | 20 | ./install.sh 21 | extism-js --version 22 | if: runner.os != 'Windows' 23 | 24 | - name: Test Install Script Part1 (Windows) 25 | run: | 26 | powershell -executionpolicy bypass -File .\install-windows.ps1 27 | if: runner.os == 'Windows' 28 | 29 | - name: Test Install Script Part2 (Windows) 30 | run: | 31 | $env:Path = "C:\Program Files\Extism\;C:\Program Files\Binaryen\;" + $env:Path 32 | extism-js --version 33 | if: runner.os == 'Windows' 34 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | compile_core: 11 | name: Compile Core 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Install Rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: default 20 | toolchain: stable 21 | target: wasm32-wasip1 22 | default: true 23 | 24 | - name: Get wasm-opt 25 | run: | 26 | curl -L https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz > binaryen.tar.gz 27 | tar xvzf binaryen.tar.gz 28 | sudo cp binaryen-version_116/bin/wasm-opt /usr/local/bin 29 | sudo chmod +x /usr/local/bin/wasm-opt 30 | 31 | - name: Install wasi-sdk 32 | run: make download-wasi-sdk 33 | 34 | - name: Make core 35 | run: make core 36 | 37 | - name: Opt core 38 | run: wasm-opt --enable-reference-types --enable-bulk-memory --strip -O3 target/wasm32-wasip1/release/js_pdk_core.wasm -o target/wasm32-wasip1/release/js_pdk_core.wasm 39 | 40 | - name: Upload core binary to artifacts 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: engine 44 | path: target/wasm32-wasip1/release/js_pdk_core.wasm 45 | 46 | compile_cli: 47 | name: Compile CLI 48 | needs: compile_core 49 | runs-on: ${{ matrix.os }} 50 | strategy: 51 | matrix: 52 | include: 53 | - name: linux 54 | os: ubuntu-latest 55 | path: target/x86_64-unknown-linux-gnu/release/extism-js 56 | asset_name: extism-js-x86_64-linux-${{ github.event.release.tag_name }} 57 | shasum_cmd: sha256sum 58 | target: x86_64-unknown-linux-gnu 59 | - name: linux-arm64 60 | os: ubuntu-latest 61 | path: target/aarch64-unknown-linux-gnu/release/extism-js 62 | asset_name: extism-js-aarch64-linux-${{ github.event.release.tag_name }} 63 | shasum_cmd: sha256sum 64 | target: aarch64-unknown-linux-gnu 65 | - name: macos 66 | os: macos-latest 67 | path: target/x86_64-apple-darwin/release/extism-js 68 | asset_name: extism-js-x86_64-macos-${{ github.event.release.tag_name }} 69 | shasum_cmd: shasum -a 256 70 | target: x86_64-apple-darwin 71 | - name: macos-arm64 72 | os: macos-latest 73 | path: target/aarch64-apple-darwin/release/extism-js 74 | asset_name: extism-js-aarch64-macos-${{ github.event.release.tag_name }} 75 | shasum_cmd: shasum -a 256 76 | target: aarch64-apple-darwin 77 | - name: windows 78 | os: windows-latest 79 | path: target\x86_64-pc-windows-msvc\release\extism-js.exe 80 | asset_name: extism-js-x86_64-windows-${{ github.event.release.tag_name }} 81 | target: x86_64-pc-windows-msvc 82 | - name: windows-arm64 83 | os: windows-latest 84 | path: target\aarch64-pc-windows-msvc\release\extism-js.exe 85 | asset_name: extism-js-aarch64-windows-${{ github.event.release.tag_name }} 86 | target: aarch64-pc-windows-msvc 87 | 88 | steps: 89 | - uses: actions/checkout@v1 90 | 91 | - uses: actions/download-artifact@v4 92 | with: 93 | name: engine 94 | path: crates/cli/ 95 | 96 | - name: ls 97 | run: ls -R 98 | working-directory: crates/cli/ 99 | 100 | - name: Install Rust 101 | uses: actions-rs/toolchain@v1 102 | with: 103 | profile: default 104 | toolchain: stable 105 | target: ${{ matrix.target }} 106 | default: true 107 | 108 | - name: Install gnu gcc 109 | run: | 110 | sudo apt-get update 111 | sudo apt-get install g++-aarch64-linux-gnu 112 | sudo apt-get install gcc-aarch64-linux-gnu 113 | if: matrix.os == 'ubuntu-latest' 114 | 115 | - name: Build CLI ${{ matrix.os }} 116 | env: 117 | EXTISM_ENGINE_PATH: js_pdk_core.wasm 118 | run: cargo build --release --target ${{ matrix.target }} --package js-pdk-cli 119 | 120 | - name: Archive assets 121 | run: gzip -k -f ${{ matrix.path }} && mv ${{ matrix.path }}.gz ${{ matrix.asset_name }}.gz 122 | 123 | - name: Upload assets to artifacts 124 | uses: actions/upload-artifact@v4 125 | with: 126 | name: ${{ matrix.asset_name }}.gz 127 | path: ${{ matrix.asset_name }}.gz 128 | 129 | - name: Upload assets to release 130 | uses: actions/upload-release-asset@v1 131 | env: 132 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | with: 134 | upload_url: ${{ github.event.release.upload_url }} 135 | asset_path: ./${{ matrix.asset_name }}.gz 136 | asset_name: ${{ matrix.asset_name }}.gz 137 | asset_content_type: application/gzip 138 | 139 | - name: Generate asset hash (Linux/MacOS) 140 | run: ${{ matrix.shasum_cmd }} ${{ matrix.asset_name }}.gz | awk '{ print $1 }' > ${{ matrix.asset_name }}.gz.sha256 141 | if: runner.os != 'Windows' 142 | 143 | - name: Generate asset hash (Windows) 144 | run: Get-FileHash -Path ${{ matrix.asset_name }}.gz -Algorithm SHA256 | Select-Object -ExpandProperty Hash > ${{ matrix.asset_name }}.gz.sha256 145 | shell: pwsh 146 | if: runner.os == 'Windows' 147 | 148 | - name: Upload asset hash to artifacts 149 | uses: actions/upload-artifact@v4 150 | with: 151 | name: ${{ matrix.asset_name }}.gz.sha256 152 | path: ${{ matrix.asset_name }}.gz.sha256 153 | 154 | - name: Upload asset hash to release 155 | uses: actions/upload-release-asset@v1 156 | env: 157 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 158 | with: 159 | upload_url: ${{ github.event.release.upload_url }} 160 | asset_path: ./${{ matrix.asset_name }}.gz.sha256 161 | asset_name: ${{ matrix.asset_name }}.gz.sha256 162 | asset_content_type: plain/text 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | wasi-sdk 4 | *.wasm 5 | .venv 6 | -------------------------------------------------------------------------------- /.vscode/ltex.dictionary.en-US.txt: -------------------------------------------------------------------------------- 1 | Extism 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/core", 5 | "crates/cli", 6 | ] 7 | 8 | [workspace.package] 9 | version = "1.5.1" 10 | edition = "2021" 11 | authors = ["The Extism Authors"] 12 | license = "BSD-Clause-3" 13 | 14 | [workspace.dependencies] 15 | anyhow = "^1.0.68" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: cli core fmt clean 2 | .DEFAULT_GOAL := cli 3 | 4 | download-wasi-sdk: 5 | ifeq ($(OS),Windows_NT) 6 | powershell -executionpolicy bypass -File .\install-wasi-sdk.ps1 7 | else 8 | sh install-wasi-sdk.sh 9 | endif 10 | 11 | install: 12 | cargo install --path crates/cli 13 | 14 | cli: core 15 | cd crates/cli && cargo build --release && cd - 16 | 17 | core: 18 | cd crates/core \ 19 | && cd src/prelude \ 20 | && npm install \ 21 | && npm run build \ 22 | && npx -y -p typescript tsc src/index.ts --lib es2020 --declaration --emitDeclarationOnly --outDir dist \ 23 | && cd ../.. \ 24 | && cargo build --release --target=wasm32-wasip1 \ 25 | && wasm-opt --enable-reference-types --enable-bulk-memory --strip -O3 ../../target/wasm32-wasip1/release/js_pdk_core.wasm -o ../../target/wasm32-wasip1/release/js_pdk_core.wasm \ 26 | && cd - 27 | 28 | fmt: fmt-core fmt-cli 29 | 30 | fmt-core: 31 | cd crates/core/ \ 32 | && cargo fmt -- --check \ 33 | && cargo clippy --target=wasm32-wasip1 -- -D warnings \ 34 | && cd - 35 | 36 | fmt-cli: 37 | cd crates/cli/ \ 38 | && cargo fmt -- --check \ 39 | && cargo clippy -- -D warnings \ 40 | && cd - 41 | 42 | clean: clean-wasi-sdk clean-cargo 43 | 44 | clean-cargo: 45 | cargo clean 46 | 47 | clean-wasi-sdk: 48 | rm -r wasi-sdk 2> /dev/null || true 49 | 50 | test: compile-examples 51 | @extism call examples/simple_js.wasm greet --wasi --input="Benjamin" 52 | @extism call examples/bundled.wasm greet --wasi --input="Benjamin" --allow-host "example.com" 53 | cd ./examples/host_funcs && go run . ../host_funcs.wasm 54 | @extism call examples/react.wasm render --wasi 55 | @extism call examples/react.wasm setState --input='{"action": "SET_SETTING", "payload": { "backgroundColor": "tomato" }}' --wasi 56 | @error_msg=$$(extism call examples/exception.wasm greet --wasi --input="Benjamin" 2>&1); \ 57 | if echo "$$error_msg" | grep -q "shibboleth"; then \ 58 | echo "Test passed - found expected error"; \ 59 | else \ 60 | echo "Test failed - did not find expected error message"; \ 61 | echo "Got: $$error_msg"; \ 62 | exit 1; \ 63 | fi 64 | @extism call examples/console.wasm greet --wasi --input="Benjamin" --log-level=debug 65 | @extism call examples/base64.wasm greet --wasi --input="Benjamin" --log-level=debug 66 | @error_msg=$$(extism call examples/try-catch.wasm greet --wasi --input="Benjamin" --log-level debug 2>&1); \ 67 | if echo "$$error_msg" | grep -q "got error"; then \ 68 | echo "Test passed - found expected error"; \ 69 | else \ 70 | echo "Test failed - did not find expected error message"; \ 71 | echo "Got: $$error_msg"; \ 72 | exit 1; \ 73 | fi 74 | 75 | compile-examples: cli 76 | cd examples/react && npm install && npm run build && cd ../.. 77 | ./target/release/extism-js examples/simple_js/script.js -i examples/simple_js/script.d.ts -o examples/simple_js.wasm 78 | cd examples/bundled && npm install && npm run build && cd ../.. 79 | ./target/release/extism-js examples/host_funcs/script.js -i examples/host_funcs/script.d.ts -o examples/host_funcs.wasm 80 | ./target/release/extism-js examples/exports/script.js -i examples/exports/script.d.ts -o examples/exports.wasm 81 | ./target/release/extism-js examples/exception/script.js -i examples/exception/script.d.ts -o examples/exception.wasm 82 | ./target/release/extism-js examples/console/script.js -i examples/console/script.d.ts -o examples/console.wasm 83 | ./target/release/extism-js examples/base64/script.js -i examples/base64/script.d.ts -o examples/base64.wasm 84 | ./target/release/extism-js examples/try-catch/script.js -i examples/try-catch/script.d.ts -o examples/try-catch.wasm 85 | 86 | kitchen: 87 | cd examples/kitchen-sink && npm install && npm run build && cd ../.. 88 | ./target/release/extism-js examples/kitchen-sink/dist/index.js -i examples/kitchen-sink/src/index.d.ts -o examples/kitchen-sink.wasm 89 | @extism call examples/kitchen-sink.wasm greet --input "Steve" --wasi --allow-host "*" --config "last_name=Manuel" 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism JavaScript PDK 2 | 3 | ![GitHub License](https://img.shields.io/github/license/extism/extism) 4 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/js-pdk) 5 | 6 | This project contains a tool that can be used to create 7 | [Extism Plug-ins](https://extism.org/docs/concepts/plug-in) in JavaScript. 8 | 9 | ## Overview 10 | 11 | This PDK uses [QuickJS](https://bellard.org/quickjs/) and 12 | [wizer](https://github.com/bytecodealliance/wizer) to run javascript as an 13 | Extism Plug-in. 14 | 15 | This is essentially a fork of [Javy](https://github.com/bytecodealliance/javy) 16 | by Shopify. We may wish to collaborate and upstream some things to them. For the 17 | time being I built this up from scratch using some of their crates, namely 18 | quickjs-wasm-rs. 19 | 20 | > Warning: This is a very bare-bones runtime. It's only for running pure JS code 21 | > and it does not expose node APIs or the browser APIs. We have limited support 22 | > for some W3C APIs (e.g. we support `Text{Encoder,Decoder}` but not `fetch`), 23 | > but many modules you take from npm will not work out of the box. There is no 24 | > support for node APIs or anything that makes syscalls typically. You'll need 25 | > to polyfill any APIs with a pure JS implementation, which is often possible, 26 | > but some things, such as controlling sockets, are not possible. Feel free to 27 | > [file an issue](https://github.com/extism/js-pdk/issues/new) if you think an 28 | > API should be supported though. 29 | 30 | ## Install the compiler 31 | 32 | We release the compiler as native binaries you can download and run. Check the 33 | [releases](https://github.com/extism/js-pdk/releases) page for the latest. 34 | 35 | ## Install Script 36 | 37 | ### Linux, macOS 38 | 39 | ```bash 40 | curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh 41 | bash install.sh 42 | ``` 43 | 44 | ### Windows 45 | 46 | > 7zip is required, you can find it [here](https://www.7-zip.org/). 47 | 48 | Open the Command Prompt as Administrator, then run : 49 | 50 | ```bash 51 | powershell Invoke-WebRequest -Uri https://raw.githubusercontent.com/extism/js-pdk/main/install-windows.ps1 -OutFile install-windows.ps1 52 | powershell -executionpolicy bypass -File .\install-windows.ps1 53 | ``` 54 | 55 | This will install extism-js and binaryen dependency under `Program File` folder 56 | (i.e. C:\Program Files\Binaryen and C:\Program Files\Extism). You must add these 57 | paths to your PATH environment variable. 58 | 59 | ### Testing the Install 60 | 61 | > _Note_: [Binaryen](https://github.com/WebAssembly/binaryen), specifically the 62 | > `wasm-merge` and `wasm-opt` tools are required as a dependency. We will try to 63 | > package this up eventually but for now it must be reachable on your machine. 64 | > You can install on mac with `brew install binaryen` or see their 65 | > [releases page](https://github.com/WebAssembly/binaryen/releases). 66 | 67 | Then run command with `-h` to see the help: 68 | 69 | ``` 70 | extism-js 1.1.1 71 | Extism JavaScript PDK Plugin Compiler 72 | 73 | USAGE: 74 | extism-js [FLAGS] [OPTIONS] 75 | 76 | FLAGS: 77 | -h, --help Prints help information 78 | --skip-opt Skip final optimization pass 79 | -V, --version Prints version information 80 | 81 | OPTIONS: 82 | -i [default: index.d.ts] 83 | -o [default: index.wasm] 84 | 85 | ARGS: 86 | 87 | ``` 88 | 89 | > **Note**: If you are using mac, you may need to tell your security system this 90 | > unsigned binary is fine. If you think this is dangerous, or can't get it to 91 | > work, see the "compile from source" section below. 92 | 93 | ## Getting Started 94 | 95 | The goal of writing an 96 | [Extism plug-in](https://extism.org/docs/concepts/plug-in) is to compile your 97 | JavaScript code to a Wasm module with exported functions that the host 98 | application can invoke. The first thing you should understand is creating an 99 | export. 100 | 101 | ### Exports 102 | 103 | Let's write a simple program that exports a `greet` function which will take a 104 | name as a string and return a greeting string. Paste this into a file 105 | `plugin.js`: 106 | 107 | ```javascript 108 | function greet() { 109 | const name = Host.inputString(); 110 | Host.outputString(`Hello, ${name}`); 111 | } 112 | 113 | module.exports = { greet }; 114 | ``` 115 | 116 | Some things to note about this code: 117 | 118 | 1. We can export functions by name using the normal `module.exports` object. 119 | This allows the host to invoke this function. Like a normal js module, 120 | functions cannot be seen from the outside without exporting them. 121 | 2. Currently, you must use 122 | [CJS Module syntax](https://nodejs.org/api/modules.html#modules-commonjs-modules) 123 | when not using a bundler. So the `export` keyword is not directly supported. 124 | See the [Using with a Bundler](#using-with-a-bundler) section for more. 125 | 3. In this PDK we code directly to the ABI. We get input from the using using 126 | `Host.input*` functions and we return data back with the `Host.output*` 127 | functions. 128 | 129 | We must also describe the Wasm interface for our plug-in. We do this with a 130 | typescript module DTS file. Here is our `plugin.d.ts` file: 131 | 132 | ```typescript 133 | declare module "main" { 134 | // Extism exports take no params and return an I32 135 | export function greet(): I32; 136 | } 137 | ``` 138 | 139 | Let's compile this to Wasm now using the `extism-js` tool: 140 | 141 | ```bash 142 | extism-js plugin.js -i plugin.d.ts -o plugin.wasm 143 | ``` 144 | 145 | We can now test `plugin.wasm` using the 146 | [Extism CLI](https://github.com/extism/cli)'s `run` command: 147 | 148 | ```bash 149 | extism call plugin.wasm greet --input="Benjamin" --wasi 150 | # => Hello, Benjamin! 151 | ``` 152 | 153 | > **Note**: Currently `wasi` must be provided for all JavaScript plug-ins even 154 | > if they don't need system access, however we're looking at how to make this 155 | > optional. 156 | 157 | > **Note**: We also have a web-based, plug-in tester called the 158 | > [Extism Playground](https://playground.extism.org/) 159 | 160 | ### More Exports: Error Handling 161 | 162 | We catch any exceptions thrown and return them as errors to the host. Suppose we 163 | want to re-write our greeting module to never greet Benjamins: 164 | 165 | ```javascript 166 | function greet() { 167 | const name = Host.inputString(); 168 | if (name === "Benjamin") { 169 | throw new Error("Sorry, we don't greet Benjamins!"); 170 | } 171 | Host.outputString(`Hello, ${name}!`); 172 | } 173 | 174 | module.exports = { greet }; 175 | ``` 176 | 177 | Now compile and run: 178 | 179 | ```bash 180 | extism-js plugin.js -i plugin.d.ts -o plugin.wasm 181 | extism call plugin.wasm greet --input="Benjamin" --wasi 182 | # => Error: Uncaught Error: Sorry, we don't greet Benjamins! 183 | # => at greet (script.js:4) 184 | # => at (script.js) 185 | echo $? # print last status code 186 | # => 1 187 | extism call plugin.wasm greet --input="Zach" --wasi 188 | # => Hello, Zach! 189 | echo $? 190 | # => 0 191 | ``` 192 | 193 | ### JSON 194 | 195 | If you want to handle more complex types, the plug-in can input and output bytes 196 | with `Host.inputBytes` and `Host.outputBytes` respectively. Those bytes can 197 | represent any complex type. A common format to use is JSON: 198 | 199 | ```javascript 200 | function sum() { 201 | const params = JSON.parse(Host.inputString()); 202 | Host.outputString(JSON.stringify({ sum: params.a + params.b })); 203 | } 204 | ``` 205 | 206 | ```bash 207 | extism call plugin.wasm sum --input='{"a": 20, "b": 21}' --wasi 208 | # => {"sum":41} 209 | ``` 210 | 211 | ### Configs 212 | 213 | Configs are key-value pairs that can be passed in by the host when creating a 214 | plug-in. These can be useful to statically configure the plug-in with some data 215 | that exists across every function call. Here is a trivial example using 216 | `Config.get`: 217 | 218 | ```javascript 219 | function greet() { 220 | const user = Config.get("user"); 221 | Host.outputString(`Hello, ${user}!`); 222 | } 223 | 224 | module.exports = { greet }; 225 | ``` 226 | 227 | To test it, the [Extism CLI](https://github.com/extism/cli) has a `--config` 228 | option that lets you pass in `key=value` pairs: 229 | 230 | ```bash 231 | extism call plugin.wasm greet --config user=Benjamin --wasi 232 | # => Hello, Benjamin! 233 | ``` 234 | 235 | ### Variables 236 | 237 | Variables are another key-value mechanism but it's a mutable data store that 238 | will persist across function calls. These variables will persist as long as the 239 | host has loaded and not freed the plug-in. You can use `Var.getBytes`, 240 | `Var.getString`, and `Var.set` to manipulate vars: 241 | 242 | ```javascript 243 | function count() { 244 | let count = Var.getString("count") || "0"; 245 | count = parseInt(count, 10); 246 | count += 1; 247 | Var.set("count", count.toString()); 248 | Host.outputString(count.toString()); 249 | } 250 | 251 | module.exports = { count }; 252 | ``` 253 | 254 | ### Logging 255 | 256 | There are several functions that can be used for logging: 257 | 258 | ```javascript 259 | function logStuff() { 260 | console.info("Info"); 261 | console.debug("Debug"); 262 | console.error("Error"); 263 | console.warn("Warning"); 264 | console.log("Log"); // Alias for console.info 265 | } 266 | 267 | module.exports = { logStuff }; 268 | ``` 269 | 270 | Running it, you need to pass a log-level flag: 271 | 272 | ``` 273 | extism call plugin.wasm logStuff --wasi --log-level=info 274 | # => 2023/10/17 14:25:00 Hello, World! 275 | ``` 276 | 277 | ### HTTP 278 | 279 | HTTP calls can be made using the synchronous API `Http.request`: 280 | 281 | ```javascript 282 | function callHttp() { 283 | const request = { 284 | method: "GET", 285 | url: "https://jsonplaceholder.typicode.com/todos/1", 286 | }; 287 | const response = Http.request(request); 288 | if (response.status != 200) { 289 | throw new Error(`Got non 200 response ${response.status}`); 290 | } 291 | Host.outputString(response.body); 292 | } 293 | 294 | module.exports = { callHttp }; 295 | ``` 296 | 297 | ### Host Functions 298 | 299 | Until the js-pdk hits 1.0, we may make changes to this API. To use host 300 | functions you need to declare a TypeScript interface `extism:host/user`: 301 | 302 | ```typescript 303 | declare module "main" { 304 | export function greet(): I32; 305 | } 306 | 307 | declare module "extism:host" { 308 | interface user { 309 | myHostFunction1(ptr: I64): I64; 310 | myHostFunction2(ptr: I64): I64; 311 | } 312 | } 313 | ``` 314 | 315 | **Note:** These functions may only use `I64` arguments, up to 5 arguments. 316 | 317 | To use these you need to use `Host.getFunctions()`: 318 | 319 | ```typescript 320 | const { myHostFunction1, myHostFunction2 } = Host.getFunctions(); 321 | ``` 322 | 323 | Calling them is a similar process to other PDKs. You need to manage the memory 324 | with the Memory object and pass across an offset as the `I64` ptr. Using the 325 | return value means dereferencing the returned `I64` ptr from Memory. 326 | 327 | ```typescript 328 | function greet() { 329 | let msg = "Hello from js 1"; 330 | let mem = Memory.fromString(msg); 331 | let offset = myHostFunction1(mem.offset); 332 | let response = Memory.find(offset).readString(); 333 | if (response != "myHostFunction1: " + msg) { 334 | throw Error(`wrong message came back from myHostFunction1: ${response}`); 335 | } 336 | 337 | msg = { hello: "world!" }; 338 | mem = Memory.fromJsonObject(msg); 339 | offset = myHostFunction2(mem.offset); 340 | response = Memory.find(offset).readJsonObject(); 341 | if (response.hello != "myHostFunction2") { 342 | throw Error(`wrong message came back from myHostFunction2: ${response}`); 343 | } 344 | 345 | Host.outputString(`Hello, World!`); 346 | } 347 | 348 | module.exports = { greet }; 349 | ``` 350 | 351 | **IMPORTANT:** Currently, a limitation in the js-pdk is that host functions may 352 | only have up to 5 arguments. 353 | 354 | ## Using with a bundler 355 | 356 | The compiler cli and core engine can now run bundled code. You will want to use 357 | a bundler if you want to want to or include modules from NPM, or write the 358 | plugin in Typescript, for example. 359 | 360 | There are 2 primary constraints to using a bundler: 361 | 362 | 1. Your compiled output must be CJS format, not ESM 363 | 2. You must target es2020 or lower 364 | 365 | ### Using with esbuild 366 | 367 | The easiest way to set this up would be to use esbuild. The following is a 368 | quickstart guide to setting up a project: 369 | 370 | ```bash 371 | # Make a new JS project 372 | mkdir extism-plugin 373 | cd extism-plugin 374 | npm init -y 375 | npm install esbuild @extism/js-pdk --save-dev 376 | mkdir src 377 | mkdir dist 378 | ``` 379 | 380 | Optionally add a `jsconfig.json` or `tsconfig.json` to improve intellisense: 381 | 382 | ```jsonc 383 | { 384 | "compilerOptions": { 385 | "lib": [], // this ensures unsupported globals aren't suggested 386 | "types": ["@extism/js-pdk"], // while this makes the IDE aware of the ones that are 387 | "noEmit": true // this is only relevant for tsconfig.json 388 | }, 389 | "include": ["src/**/*"] 390 | } 391 | ``` 392 | 393 | Add `esbuild.js`: 394 | 395 | ```js 396 | const esbuild = require("esbuild"); 397 | // include this if you need some node support: 398 | // npm i @esbuild-plugins/node-modules-polyfill --save-dev 399 | // const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill') 400 | 401 | esbuild 402 | .build({ 403 | // supports other types like js or ts 404 | entryPoints: ["src/index.js"], 405 | outdir: "dist", 406 | bundle: true, 407 | sourcemap: true, 408 | //plugins: [NodeModulesPolyfillPlugin()], // include this if you need some node support 409 | minify: false, // might want to use true for production build 410 | format: "cjs", // needs to be CJS for now 411 | target: ["es2020"], // don't go over es2020 because quickjs doesn't support it 412 | }); 413 | ``` 414 | 415 | Add a `build` script to your `package.json`: 416 | 417 | ```json 418 | { 419 | "name": "extism-plugin", 420 | // ... 421 | "scripts": { 422 | // ... 423 | "build": "node esbuild.js && extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm" 424 | } 425 | // ... 426 | } 427 | ``` 428 | 429 | Let's import a module from NPM: 430 | 431 | ```bash 432 | npm install --save fastest-levenshtein 433 | ``` 434 | 435 | Now make some code in `src/index.js`. You can use `import` to load node_modules: 436 | 437 | > **Note**: This module uses the ESM Module syntax. The bundler will transform 438 | > all the code to CJS for us 439 | 440 | ```js 441 | import { closest, distance } from "fastest-levenshtein"; 442 | 443 | // this function is private to the module 444 | function privateFunc() { 445 | return "world"; 446 | } 447 | 448 | // use any export syntax to export a function be callable by the extism host 449 | export function get_closest() { 450 | let input = Host.inputString(); 451 | let result = closest(input, ["slow", "faster", "fastest"]); 452 | Host.outputString(result + " " + privateFunc()); 453 | } 454 | ``` 455 | 456 | And a d.ts file for it at `src/index.d.ts`: 457 | 458 | ```typescript 459 | declare module "main" { 460 | // Extism exports take no params and return an I32 461 | export function get_closest(): I32; 462 | } 463 | ``` 464 | 465 | ```bash 466 | # Run the build script and the plugin will be compiled to dist/plugin.wasm 467 | npm run build 468 | # You can now call from the extism cli or a host SDK 469 | extism call dist/plugin.wasm get_closest --input="fest" --wasi 470 | # => faster World 471 | ``` 472 | 473 | ## Using with React and JSX / TSX 474 | 475 | Oftentimes people want their JS plug-ins to control or create views. React and 476 | JSX are a great way to do this. Here is the simplest example. Let's just render 477 | a simple view in a typescript plugin. 478 | 479 | First declare a `render` export: 480 | 481 | ```typescript 482 | declare module "main" { 483 | export function render(): I32; 484 | } 485 | ``` 486 | 487 | Now install the deps: 488 | 489 | ```bash 490 | npm install react-dom --save 491 | npm install @types/react --save-dev 492 | ``` 493 | 494 | Now we can make an index.tsx: 495 | 496 | ```typescript 497 | import { renderToString } from "react-dom/server"; 498 | import React from "react"; 499 | 500 | interface AppProps { 501 | name: string; 502 | } 503 | 504 | function App(props: AppProps) { 505 | return ( 506 | <> 507 |

Hello ${props.name}!

508 | 509 | ); 510 | } 511 | 512 | export function render() { 513 | const props = JSON.parse(Host.inputString()) as AppProps; 514 | const app = ; 515 | Host.outputString(renderToString(app)); 516 | } 517 | ``` 518 | 519 | To see a more complex example of how you might build something real, see 520 | [examples/react](./examples/react/) 521 | 522 | ## Generating Bindings 523 | 524 | It's often very useful to define a schema to describe the function signatures 525 | and types you want to use between Extism SDK and PDK languages. 526 | 527 | [XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source 528 | framework to generate PDK bindings for Extism plug-ins. It's used by the 529 | [XTP Platform](https://www.getxtp.com/), but can be used outside of the platform 530 | to define any Extism compatible plug-in system. 531 | 532 | ### 1. Install the `xtp` CLI. 533 | 534 | See installation instructions 535 | [here](https://docs.xtp.dylibso.com/docs/cli#installation). 536 | 537 | ### 2. Create a schema using our OpenAPI-inspired IDL: 538 | 539 | ```yaml 540 | version: v1-draft 541 | exports: 542 | CountVowels: 543 | input: 544 | type: string 545 | contentType: text/plain; charset=utf-8 546 | output: 547 | $ref: "#/components/schemas/VowelReport" 548 | contentType: application/json 549 | # components.schemas defined in example-schema.yaml... 550 | ``` 551 | 552 | > See an example in [example-schema.yaml](./example-schema.yaml), or a full 553 | > "kitchen sink" example on 554 | > [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/). 555 | 556 | ### 3. Generate bindings to use from your plugins: 557 | 558 | ``` 559 | xtp plugin init --schema-file ./example-schema.yaml 560 | > 1. TypeScript 561 | 2. Go 562 | 3. Rust 563 | 4. Python 564 | 5. C# 565 | 6. Zig 566 | 7. C++ 567 | 8. GitHub Template 568 | 9. Local Template 569 | ``` 570 | 571 | This will create an entire boilerplate plugin project for you to get started 572 | with: 573 | 574 | ```typescript 575 | /** 576 | * @returns {VowelReport} The result of counting vowels on the Vowels input. 577 | */ 578 | export function CountVowelsImpl(input: string): VowelReport { 579 | // TODO: fill out your implementation here 580 | throw new Error("Function not implemented."); 581 | } 582 | ``` 583 | 584 | Implement the empty function(s), and run `xtp plugin build` to compile your 585 | plugin. 586 | 587 | > For more information about XTP Bindgen, see the 588 | > [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and 589 | > the official 590 | > [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema). 591 | 592 | ## Compiling the compiler from source 593 | 594 | ### Prerequisites 595 | 596 | Before compiling the compiler, you need to install prerequisites. 597 | 598 | 1. Install Rust using [rustup](https://rustup.rs) 599 | 2. Install the WASI target platform via 600 | `rustup target add --toolchain stable wasm32-wasip1` 601 | 3. Install the wasi sdk using the makefile command: `make download-wasi-sdk` 602 | 4. Install [CMake](https://cmake.org/install/) (on macOS with homebrew, 603 | `brew install cmake`) 604 | 5. Install [Binaryen](https://github.com/WebAssembly/binaryen/) and add it's 605 | install location to your PATH (only wasm-opt is required for build process) 606 | 6. Install [7zip](https://www.7-zip.org/)(only for Windows) 607 | 608 | ### Compiling from source 609 | 610 | Run make to compile the core crate (the engine) and the cli: 611 | 612 | ``` 613 | make 614 | ``` 615 | 616 | To test the built compiler (ensure you have Extism installed): 617 | 618 | ```bash 619 | ./target/release/extism-js bundle.js -i bundle.d.ts -o out.wasm 620 | extism call out.wasm count_vowels --wasi --input='Hello World Test!' 621 | # => "{\"count\":4}" 622 | ``` 623 | 624 | ## How it works 625 | 626 | This works a little differently than other PDKs. You cannot compile JS to Wasm 627 | because it doesn't have an appropriate type system to do this. Something like 628 | [Assemblyscript](https://github.com/extism/assemblyscript-pdk) is better suited 629 | for this. Instead, we have compiled QuickJS to Wasm. The `extism-js` command we 630 | have provided here is a little compiler / wrapper that does a series of things 631 | for you: 632 | 633 | 1. It loads an "engine" Wasm program containing the QuickJS runtime 634 | 2. It initializes a QuickJS context 635 | 3. It loads your js source code into memory 636 | 4. It parses the js source code for exports and generates 1-to-1 proxy export 637 | functions in Wasm 638 | 5. It freezes and emits the machine state as a new Wasm file at this 639 | post-initialized point in time 640 | 641 | This new Wasm file can be used just like any other Extism plugin. 642 | 643 | ## Why not use Javy? 644 | 645 | Javy, and many other high level language Wasm tools, assume use of the _command 646 | pattern_. This is when the Wasm module only exports a main function and 647 | communicates with the host through stdin and stdout. With Extism, we have more 648 | of a shared library interface. The module exposes multiple entry points through 649 | exported functions. Furthermore, Javy has many Javy and Shopify specific things 650 | it's doing that we will not need. However, the core idea is the same, and we can 651 | possibly contribute by adding support to Javy for non-command-pattern modules. 652 | Then separating the Extism PDK specific stuff into another repo. 653 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "js-pdk-cli" 3 | authors.workspace = true 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | build = "build.rs" 8 | 9 | [[bin]] 10 | name = "extism-js" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | anyhow = { workspace = true } 15 | wizer = "4" 16 | structopt = "0.3" 17 | swc_atoms = "0.6.5" 18 | swc_common = "0.33.10" 19 | swc_ecma_ast = "0.112" 20 | swc_ecma_parser = "0.143" 21 | wagen = "0.1" 22 | log = "0.4" 23 | tempfile = "3" 24 | env_logger = "0.11" 25 | -------------------------------------------------------------------------------- /crates/cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | fn main() { 6 | if let Ok("cargo-clippy") = env::var("CARGO_CFG_FEATURE").as_ref().map(String::as_str) { 7 | stub_engine_for_clippy(); 8 | } else { 9 | copy_engine_binary(); 10 | } 11 | } 12 | 13 | // When using clippy, we need to write a stubbed engine.wasm file to ensure compilation succeeds. This 14 | // skips building the actual engine.wasm binary that would be injected into the CLI binary. 15 | fn stub_engine_for_clippy() { 16 | let engine_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("engine.wasm"); 17 | 18 | if !engine_path.exists() { 19 | std::fs::write(engine_path, []).expect("failed to write empty engine.wasm stub"); 20 | println!("cargo:warning=using stubbed engine.wasm for static analysis purposes..."); 21 | } 22 | } 23 | 24 | // Copy the engine binary build from the `core` crate 25 | fn copy_engine_binary() { 26 | let override_engine_path = env::var("EXTISM_ENGINE_PATH"); 27 | let is_override = override_engine_path.is_ok(); 28 | let mut engine_path = PathBuf::from( 29 | override_engine_path.unwrap_or_else(|_| env::var("CARGO_MANIFEST_DIR").unwrap()), 30 | ); 31 | 32 | if !is_override { 33 | engine_path.pop(); 34 | engine_path.pop(); 35 | engine_path = engine_path.join("target/wasm32-wasip1/release/js_pdk_core.wasm"); 36 | } 37 | 38 | println!("cargo:rerun-if-changed={}", engine_path.to_str().unwrap()); 39 | println!("cargo:rerun-if-changed=build.rs"); 40 | 41 | if engine_path.exists() { 42 | let copied_engine_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("engine.wasm"); 43 | fs::copy(&engine_path, copied_engine_path).unwrap(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/cli/script.js: -------------------------------------------------------------------------------- 1 | function add(a, b) { 2 | return 'Hello' 3 | } 4 | 5 | exports = {add} 6 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod opt; 2 | mod options; 3 | mod shims; 4 | mod ts_parser; 5 | 6 | use crate::options::Options; 7 | use crate::ts_parser::parse_interface_file; 8 | use anyhow::{bail, Result}; 9 | use log::LevelFilter; 10 | use shims::generate_wasm_shims; 11 | use std::env; 12 | use std::path::PathBuf; 13 | use std::process::Stdio; 14 | use std::{fs, io::Write, process::Command}; 15 | use structopt::StructOpt; 16 | use tempfile::TempDir; 17 | 18 | const CORE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/engine.wasm")); 19 | 20 | fn main() -> Result<()> { 21 | let mut builder = env_logger::Builder::new(); 22 | builder 23 | .filter(None, LevelFilter::Info) 24 | .target(env_logger::Target::Stdout) 25 | .init(); 26 | 27 | let opts = Options::from_args(); 28 | 29 | if opts.core { 30 | opt::Optimizer::new(CORE) 31 | .wizen(true) 32 | .write_optimized_wasm(opts.output)?; 33 | return Ok(()); 34 | } 35 | 36 | // We need to parse the interface.d.ts file 37 | let interface_path = PathBuf::from(&opts.interface_file); 38 | if !interface_path.exists() { 39 | bail!( 40 | "Could not find interface file {}. Set to a valid d.ts file with the -i flag", 41 | &interface_path.to_str().unwrap() 42 | ); 43 | } 44 | let plugin_interface = parse_interface_file(&interface_path)?; 45 | 46 | // Copy in the user's js code from the configured file 47 | let mut user_code = fs::read(&opts.input_js)?; 48 | 49 | // If we have imports, we need to inject some state needed for host function support 50 | let mut contents = Vec::new(); 51 | let mut names = Vec::new(); 52 | let mut sorted_names = Vec::new(); 53 | for ns in &plugin_interface.imports { 54 | sorted_names.extend(ns.functions.iter().map(|s| (&s.name, s.results.len()))); 55 | } 56 | sorted_names.sort_by_key(|x| x.0.as_str()); 57 | 58 | for (name, results) in sorted_names { 59 | names.push(format!("{{ name: '{}', results: {} }}", &name, results)); 60 | } 61 | 62 | contents 63 | .extend_from_slice(format!("Host.__hostFunctions = [{}];\n", names.join(", ")).as_bytes()); 64 | contents.append(&mut user_code); 65 | 66 | // Create a tmp dir to hold all the library objects 67 | // This can go away once we do all the wasm-merge stuff in process 68 | let tmp_dir = TempDir::new()?; 69 | let core_path = tmp_dir.path().join("core.wasm"); 70 | let shim_path = tmp_dir.path().join("shim.wasm"); 71 | 72 | // First wizen the core module 73 | let self_cmd = env::args().next().expect("Expected a command argument"); 74 | { 75 | let mut command = Command::new(self_cmd) 76 | .arg("-c") 77 | .arg(&opts.input_js) 78 | .arg("-o") 79 | .arg(&core_path) 80 | .stdin(Stdio::piped()) 81 | .spawn()?; 82 | command 83 | .stdin 84 | .take() 85 | .expect("Expected to get writeable stdin") 86 | .write_all(&contents)?; 87 | let status = command.wait()?; 88 | if !status.success() { 89 | bail!("Couldn't create wasm from input"); 90 | } 91 | } 92 | 93 | // Create our shim file given our parsed TS module object 94 | generate_wasm_shims( 95 | &shim_path, 96 | &plugin_interface.exports, 97 | &plugin_interface.imports, 98 | )?; 99 | 100 | let output = Command::new("wasm-merge") 101 | .arg("--version") 102 | .stdout(Stdio::null()) 103 | .stderr(Stdio::null()) 104 | .status(); 105 | if output.is_err() { 106 | bail!("Failed to detect wasm-merge. Please install binaryen and make sure wasm-merge is on your path: https://github.com/WebAssembly/binaryen"); 107 | } 108 | 109 | // Merge the shim with the core module 110 | let status = Command::new("wasm-merge") 111 | .arg(&core_path) 112 | .arg("core") 113 | .arg(&shim_path) 114 | .arg("shim") 115 | .arg("-o") 116 | .arg(&opts.output) 117 | .arg("--enable-reference-types") 118 | .arg("--enable-bulk-memory") 119 | .status()?; 120 | if !status.success() { 121 | bail!("wasm-merge failed. Couldn't merge shim"); 122 | } 123 | 124 | if !opts.skip_opt { 125 | opt::optimize_wasm_file(opts.output)?; 126 | } 127 | 128 | Ok(()) 129 | } 130 | -------------------------------------------------------------------------------- /crates/cli/src/opt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use std::{ 3 | path::Path, 4 | process::{Command, Stdio}, 5 | }; 6 | use wizer::Wizer; 7 | 8 | pub(crate) struct Optimizer<'a> { 9 | wizen: bool, 10 | optimize: bool, 11 | wasm: &'a [u8], 12 | } 13 | 14 | impl<'a> Optimizer<'a> { 15 | pub fn new(wasm: &'a [u8]) -> Self { 16 | Self { 17 | wasm, 18 | optimize: false, 19 | wizen: false, 20 | } 21 | } 22 | 23 | #[allow(unused)] 24 | pub fn optimize(self, optimize: bool) -> Self { 25 | Self { optimize, ..self } 26 | } 27 | 28 | pub fn wizen(self, wizen: bool) -> Self { 29 | Self { wizen, ..self } 30 | } 31 | 32 | pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { 33 | if self.wizen { 34 | let wasm = Wizer::new() 35 | .allow_wasi(true)? 36 | .inherit_stdio(true) 37 | .wasm_bulk_memory(true) 38 | .run(self.wasm)?; 39 | std::fs::write(&dest, wasm)?; 40 | } else { 41 | std::fs::write(&dest, self.wasm)?; 42 | } 43 | 44 | if self.optimize { 45 | optimize_wasm_file(dest)?; 46 | } 47 | 48 | Ok(()) 49 | } 50 | } 51 | 52 | pub(crate) fn optimize_wasm_file(dest: impl AsRef) -> Result<(), Error> { 53 | let output = Command::new("wasm-opt") 54 | .arg("--version") 55 | .stdout(Stdio::null()) 56 | .stderr(Stdio::null()) 57 | .status(); 58 | if output.is_err() { 59 | anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); 60 | } 61 | Command::new("wasm-opt") 62 | .arg("--enable-reference-types") 63 | .arg("--enable-bulk-memory") 64 | .arg("--strip") 65 | .arg("-O3") 66 | .arg(dest.as_ref()) 67 | .arg("-o") 68 | .arg(dest.as_ref()) 69 | .status()?; 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /crates/cli/src/options.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::StructOpt; 3 | 4 | #[derive(Debug, StructOpt)] 5 | #[structopt(name = "extism-js", about = "Extism JavaScript PDK Plugin Compiler")] 6 | pub struct Options { 7 | #[structopt( 8 | parse(from_os_str), 9 | about = "Input JS program for the plugin. Needs to be a single file." 10 | )] 11 | pub input_js: PathBuf, 12 | 13 | #[structopt( 14 | short = "i", 15 | parse(from_os_str), 16 | default_value = "index.d.ts", 17 | about = "d.ts file describing the plug-in interface." 18 | )] 19 | pub interface_file: PathBuf, 20 | 21 | #[structopt( 22 | short = "o", 23 | parse(from_os_str), 24 | default_value = "index.wasm", 25 | about = "Ouput wasm file." 26 | )] 27 | pub output: PathBuf, 28 | 29 | #[structopt(short = "c", about = "Include the core")] 30 | pub core: bool, 31 | 32 | #[structopt(long = "--skip-opt", about = "Skip final optimization pass")] 33 | pub skip_opt: bool, 34 | } 35 | -------------------------------------------------------------------------------- /crates/cli/src/shims.rs: -------------------------------------------------------------------------------- 1 | use crate::ts_parser::Interface; 2 | use anyhow::Result; 3 | use std::path::Path; 4 | use wagen::{BlockType, Instr, ValType}; 5 | 6 | #[derive(PartialEq)] 7 | enum TypeCode { 8 | Void = 0, 9 | I32 = 1, 10 | I64 = 2, 11 | F32 = 3, 12 | F64 = 4, 13 | } 14 | 15 | pub fn generate_wasm_shims( 16 | path: impl AsRef, 17 | exports: &Interface, 18 | imports: &[Interface], 19 | ) -> Result<()> { 20 | let mut module = wagen::Module::new(); 21 | 22 | let __arg_start = module.import("core", "__arg_start", None, [], []); 23 | let __arg_i32 = module.import("core", "__arg_i32", None, [ValType::I32], []); 24 | let __arg_i64 = module.import("core", "__arg_i64", None, [ValType::I64], []); 25 | let __arg_f32 = module.import("core", "__arg_f32", None, [ValType::F32], []); 26 | let __arg_f64 = module.import("core", "__arg_f64", None, [ValType::F64], []); 27 | let __invoke_i32 = module.import("core", "__invoke_i32", None, [ValType::I32], [ValType::I32]); 28 | let __invoke_i64 = module.import("core", "__invoke_i64", None, [ValType::I32], [ValType::I64]); 29 | let __invoke_f32 = module.import("core", "__invoke_f32", None, [ValType::I32], [ValType::F32]); 30 | let __invoke_f64 = module.import("core", "__invoke_f64", None, [ValType::I32], [ValType::F64]); 31 | let __invoke = module.import("core", "__invoke", None, [ValType::I32], []); 32 | 33 | let mut import_elements = Vec::new(); 34 | let mut import_items = vec![]; 35 | for import in imports.iter() { 36 | for f in import.functions.iter() { 37 | let params: Vec<_> = f.params.iter().map(|x| x.ptype).collect(); 38 | let results: Vec<_> = f.results.iter().map(|x| x.ptype).collect(); 39 | let index = module.import(&import.name, &f.name, None, params.clone(), results.clone()); 40 | import_items.push((f.name.clone(), index, params, results)); 41 | } 42 | } 43 | import_items.sort_by_key(|x| x.0.to_string()); 44 | 45 | for (_name, index, _params, _results) in &import_items { 46 | import_elements.push(index.index()); 47 | } 48 | 49 | let table_min = import_elements.len() as u32; 50 | 51 | let import_table = module.tables().push(wagen::TableType { 52 | element_type: wagen::RefType::FUNCREF, 53 | minimum: table_min, 54 | maximum: None, 55 | }); 56 | 57 | let mut get_function_return_type_builder = wagen::Builder::default(); 58 | 59 | for (func_idx, (_name, _index, _params, results)) in import_items.iter().enumerate() { 60 | let type_code = results.first().map_or(TypeCode::Void, |val_type| match val_type { 61 | ValType::I32 => TypeCode::I32, 62 | ValType::I64 => TypeCode::I64, 63 | ValType::F32 => TypeCode::F32, 64 | ValType::F64 => TypeCode::F64, 65 | _ => TypeCode::Void, 66 | }); 67 | 68 | if type_code == TypeCode::Void { 69 | continue; 70 | } 71 | 72 | // Compare the input function index with the current index. 73 | get_function_return_type_builder.push(Instr::LocalGet(0)); // load requested function index 74 | get_function_return_type_builder.push(Instr::I32Const(func_idx as i32)); // load func_idx 75 | get_function_return_type_builder.push(Instr::I32Eq); // compare 76 | get_function_return_type_builder.push(Instr::If(BlockType::Empty)); // if true 77 | get_function_return_type_builder.push(Instr::I32Const(type_code as i32)); // load type code 78 | get_function_return_type_builder.push(Instr::Return); // early return if match 79 | get_function_return_type_builder.push(Instr::End); 80 | } 81 | 82 | get_function_return_type_builder.push(Instr::I32Const(0)); // Default to 0 83 | get_function_return_type_builder.push(Instr::Return); 84 | 85 | let get_function_return_type_func = module.func( 86 | "__get_function_return_type", 87 | vec![ValType::I32], // takes function index 88 | vec![ValType::I32], // returns type code 89 | vec![], 90 | ); 91 | get_function_return_type_func.export("__get_function_return_type"); 92 | get_function_return_type_func.body = get_function_return_type_builder; 93 | 94 | let mut get_function_arg_type_builder = wagen::Builder::default(); 95 | 96 | for (func_idx, (_name, _index, params, _results)) in import_items.iter().enumerate() { 97 | for arg_idx in 0..params.len() { 98 | let type_code = match params[arg_idx] { 99 | ValType::I32 => TypeCode::I32, 100 | ValType::I64 => TypeCode::I64, 101 | ValType::F32 => TypeCode::F32, 102 | ValType::F64 => TypeCode::F64, 103 | _ => panic!("Unsupported argument type for function {} at index {}", func_idx, arg_idx), 104 | }; 105 | 106 | // Compare both function index and argument index 107 | get_function_arg_type_builder.push(Instr::LocalGet(0)); // function index 108 | get_function_arg_type_builder.push(Instr::I32Const(func_idx as i32)); 109 | get_function_arg_type_builder.push(Instr::I32Eq); 110 | 111 | get_function_arg_type_builder.push(Instr::LocalGet(1)); // argument index 112 | get_function_arg_type_builder.push(Instr::I32Const(arg_idx as i32)); 113 | get_function_arg_type_builder.push(Instr::I32Eq); 114 | 115 | get_function_arg_type_builder.push(Instr::I32And); // Both must match 116 | 117 | // If both match, return the type code 118 | get_function_arg_type_builder.push(Instr::If(BlockType::Empty)); 119 | get_function_arg_type_builder.push(Instr::I32Const(type_code as i32)); 120 | get_function_arg_type_builder.push(Instr::Return); 121 | get_function_arg_type_builder.push(Instr::End); 122 | } 123 | } 124 | 125 | // Default return if no match 126 | get_function_arg_type_builder.push(Instr::I32Const(0)); 127 | get_function_arg_type_builder.push(Instr::Return); 128 | 129 | let get_function_arg_type_func = module.func( 130 | "__get_function_arg_type", 131 | vec![ValType::I32, ValType::I32], // takes (function_index, arg_index) 132 | vec![ValType::I32], // returns type code 133 | vec![], 134 | ); 135 | get_function_arg_type_func.export("__get_function_arg_type"); 136 | get_function_arg_type_func.body = get_function_arg_type_builder; 137 | 138 | // Create converters for each host function to reinterpret the I64 bit pattern as the expected type 139 | let mut converter_indices = Vec::new(); 140 | for (_, (name, _index, params, results)) in import_items.iter().enumerate() { 141 | let import_type = module 142 | .types() 143 | .push(|t| t.function(params.clone(), results.clone())); 144 | 145 | let mut builder = wagen::Builder::default(); 146 | 147 | // Convert input parameters 148 | for (i, param) in params.iter().enumerate() { 149 | builder.push(Instr::LocalGet((i + 1) as u32)); // skip function index param 150 | 151 | match param { 152 | ValType::I32 => { 153 | builder.push(Instr::I32WrapI64); 154 | } 155 | ValType::I64 => { 156 | // No conversion needed - already i64 157 | } 158 | ValType::F32 => { 159 | // Input is already the bit pattern from globals.rs convert_to_u64_bits 160 | // First truncate to i32 then reinterpret as f32 161 | builder.push(Instr::I32WrapI64); 162 | builder.push(Instr::F32ReinterpretI32); 163 | } 164 | ValType::F64 => { 165 | // Input is already the bit pattern from JS DataView 166 | // Just reinterpret the i64 as f64 167 | builder.push(Instr::F64ReinterpretI64); 168 | } 169 | r => { 170 | anyhow::bail!("Unsupported param type: {:?}", r); 171 | } 172 | } 173 | } 174 | 175 | // Call the imported function 176 | builder.push(Instr::LocalGet(0)); 177 | builder.push(Instr::CallIndirect { 178 | ty: import_type, 179 | table: import_table, 180 | }); 181 | 182 | // Convert result back to i64 bits for JS 183 | if let Some(result) = results.first() { 184 | match result { 185 | ValType::I32 => { 186 | builder.push(Instr::I64ExtendI32U); 187 | } 188 | ValType::I64 => { 189 | // Already i64, no conversion needed 190 | } 191 | ValType::F32 => { 192 | // Convert f32 to its bit pattern 193 | builder.push(Instr::I32ReinterpretF32); 194 | builder.push(Instr::I64ExtendI32U); 195 | } 196 | ValType::F64 => { 197 | // Convert f64 to its bit pattern 198 | builder.push(Instr::I64ReinterpretF64); 199 | } 200 | r => { 201 | anyhow::bail!("Unsupported result type: {:?}", r); 202 | } 203 | } 204 | } else { 205 | // No return value, push 0 206 | builder.push(Instr::I64Const(0)); 207 | } 208 | 209 | // Create the converter function 210 | let mut shim_params = vec![ValType::I32]; // Function index 211 | shim_params.extend(std::iter::repeat(ValType::I64).take(params.len())); 212 | 213 | let conv_func = module.func( 214 | &format!("__conv_{}", name), 215 | shim_params, 216 | vec![ValType::I64], 217 | vec![], 218 | ); 219 | conv_func.export(&format!("__conv_{}", name)); 220 | conv_func.body = builder; 221 | 222 | converter_indices.push(conv_func.index); 223 | } 224 | 225 | let router = module.func( 226 | "__invokeHostFunc", 227 | vec![ 228 | ValType::I32, // func_idx 229 | ValType::I64, // args[0] 230 | ValType::I64, // args[1] 231 | ValType::I64, // args[2] 232 | ValType::I64, // args[3] 233 | ValType::I64, // args[4] 234 | ], 235 | vec![ValType::I64], 236 | vec![], 237 | ); 238 | 239 | // Similar builder logic as before but simplified to one function 240 | let mut router_builder = wagen::Builder::default(); 241 | 242 | for (func_idx, (_name, _index, params, _results)) in import_items.iter().enumerate() { 243 | router_builder.push(Instr::LocalGet(0)); // func index 244 | router_builder.push(Instr::I32Const(func_idx as i32)); 245 | router_builder.push(Instr::I32Eq); 246 | router_builder.push(Instr::If(BlockType::Empty)); 247 | 248 | // First push func_idx for converter 249 | router_builder.push(Instr::LocalGet(0)); 250 | 251 | // Then push remaining args from router's inputs 252 | for (i, _) in params.iter().enumerate() { 253 | router_builder.push(Instr::LocalGet((i + 1) as u32)); 254 | } 255 | 256 | router_builder.push(Instr::Call(converter_indices[func_idx])); 257 | router_builder.push(Instr::Return); 258 | router_builder.push(Instr::End); 259 | } 260 | 261 | router_builder.push(Instr::I64Const(0)); 262 | router_builder.push(Instr::Return); 263 | 264 | router.export("__invokeHostFunc"); 265 | router.body = router_builder; 266 | 267 | // Set up the table 268 | module.active_element( 269 | Some(import_table), 270 | wagen::Elements::Functions(&import_elements), 271 | ); 272 | 273 | // Generate exports 274 | for (idx, export) in exports.functions.iter().enumerate() { 275 | let params: Vec<_> = export.params.iter().map(|x| x.ptype).collect(); 276 | let results: Vec<_> = export.results.iter().map(|x| x.ptype).collect(); 277 | if results.len() > 1 { 278 | anyhow::bail!( 279 | "Multiple return arguments are not currently supported but used in exported function {}", 280 | export.name 281 | ); 282 | } 283 | 284 | let mut builder = wagen::Builder::default(); 285 | builder.push(Instr::Call(__arg_start.index())); 286 | 287 | for (parami, param) in params.iter().enumerate() { 288 | builder.push(Instr::LocalGet(parami as u32)); 289 | 290 | match param { 291 | ValType::I32 => { 292 | builder.push(Instr::Call(__arg_i32.index())); 293 | } 294 | ValType::I64 => { 295 | builder.push(Instr::Call(__arg_i64.index())); 296 | } 297 | ValType::F32 => { 298 | builder.push(Instr::Call(__arg_f32.index())); 299 | } 300 | ValType::F64 => { 301 | builder.push(Instr::Call(__arg_f64.index())); 302 | } 303 | r => { 304 | anyhow::bail!("Unsupported param type: {:?}", r); 305 | } 306 | } 307 | } 308 | 309 | builder.push(Instr::I32Const(idx as i32)); 310 | match results.first() { 311 | None => { 312 | builder.push(Instr::Call(__invoke.index())); 313 | } 314 | Some(ValType::I32) => { 315 | builder.push(Instr::Call(__invoke_i32.index())); 316 | } 317 | Some(ValType::I64) => { 318 | builder.push(Instr::Call(__invoke_i64.index())); 319 | } 320 | Some(ValType::F32) => { 321 | builder.push(Instr::Call(__invoke_f32.index())); 322 | } 323 | Some(ValType::F64) => { 324 | builder.push(Instr::Call(__invoke_f64.index())); 325 | } 326 | Some(r) => { 327 | anyhow::bail!("Unsupported result type: {:?}", r); 328 | } 329 | } 330 | 331 | let f = module.func(&export.name, params, results, vec![]); 332 | f.export(&export.name); 333 | f.body = builder; 334 | } 335 | 336 | // Validation with debug output 337 | if let Err(error) = module.clone().validate_save(path.as_ref()) { 338 | eprintln!("Validation failed: {:?}", error); 339 | module.save("/tmp/wizer/incomplete_shim.wasm")?; 340 | return Err(error); 341 | } 342 | 343 | Ok(()) 344 | } 345 | -------------------------------------------------------------------------------- /crates/cli/src/ts_parser.rs: -------------------------------------------------------------------------------- 1 | extern crate swc_common; 2 | extern crate swc_ecma_parser; 3 | use std::path::Path; 4 | 5 | use anyhow::{bail, Context, Result}; 6 | use wagen::ValType; 7 | 8 | use swc_common::sync::Lrc; 9 | use swc_common::SourceMap; 10 | use swc_ecma_ast::{ 11 | Decl, Module, ModuleDecl, Stmt, TsInterfaceDecl, TsKeywordTypeKind, TsModuleDecl, TsType, 12 | }; 13 | use swc_ecma_ast::{ModuleItem, TsTypeElement}; 14 | use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct Param { 18 | #[allow(unused)] 19 | pub name: String, 20 | pub ptype: ValType, 21 | } 22 | 23 | impl Param { 24 | pub fn new(name: &str, ptype: ValType) -> Param { 25 | Param { 26 | name: name.to_string(), 27 | ptype, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub struct Signature { 34 | pub name: String, 35 | pub params: Vec, 36 | pub results: Vec, 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | pub struct Interface { 41 | pub name: String, 42 | pub functions: Vec, 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct PluginInterface { 47 | pub exports: Interface, 48 | pub imports: Vec, 49 | } 50 | 51 | pub fn val_type(s: &str) -> Result { 52 | match s.to_ascii_lowercase().as_str() { 53 | "i32" => Ok(ValType::I32), 54 | "i64" | "ptr" => Ok(ValType::I64), 55 | "f32" => Ok(ValType::F32), 56 | "f64" => Ok(ValType::F64), 57 | _ => anyhow::bail!("Unsupported type: {}", s), // Extism handle 58 | } 59 | } 60 | 61 | pub fn param_type(params: &mut Vec, vn: &str, t: &TsType) -> Result<()> { 62 | let typ = if let Some(t) = t.as_ts_type_ref() { 63 | t.type_name 64 | .as_ident() 65 | .context("Illegal param type")? 66 | .sym 67 | .as_str() 68 | } else { 69 | anyhow::bail!("Unsupported param type: {:?}", t); 70 | }; 71 | params.push(Param::new(vn, val_type(typ)?)); 72 | Ok(()) 73 | } 74 | 75 | pub fn result_type(results: &mut Vec, return_type: &TsType) -> Result<()> { 76 | let return_type = if let Some(return_type) = return_type.as_ts_type_ref() { 77 | Some( 78 | return_type 79 | .type_name 80 | .as_ident() 81 | .context("Illegal return type")? 82 | .sym 83 | .as_str(), 84 | ) 85 | } else if let Some(t) = return_type.as_ts_keyword_type() { 86 | match t.kind { 87 | TsKeywordTypeKind::TsVoidKeyword => None, 88 | _ => anyhow::bail!("Unsupported return type: {:?}", t.kind), 89 | } 90 | } else { 91 | anyhow::bail!("Unsupported return type: {:?}", return_type) 92 | }; 93 | if let Some(r) = return_type { 94 | results.push(Param::new("result", val_type(r)?)); 95 | } 96 | Ok(()) 97 | } 98 | 99 | /// Parses the non-main parts of the module which maps to the wasm imports 100 | fn parse_user_interface(i: &TsInterfaceDecl) -> Result { 101 | let mut signatures = Vec::new(); 102 | let name = i.id.sym.as_str(); 103 | for sig in &i.body.body { 104 | match sig { 105 | TsTypeElement::TsMethodSignature(t) => { 106 | let name = t.key.as_ident().unwrap().sym.to_string(); 107 | let mut params = vec![]; 108 | let mut results = vec![]; 109 | 110 | for p in t.params.iter() { 111 | let vn = p.as_ident().unwrap().id.sym.as_str(); 112 | let typ = p.as_ident().unwrap().type_ann.clone(); 113 | let t = typ.unwrap().type_ann; 114 | param_type(&mut params, vn, &t)?; 115 | } 116 | if params.len() > 5 { 117 | anyhow::bail!("Host functions only support up to 5 arguments"); 118 | } 119 | if let Some(return_type) = &t.type_ann { 120 | result_type(&mut results, &return_type.type_ann)?; 121 | } 122 | let signature = Signature { 123 | name, 124 | params, 125 | results, 126 | }; 127 | signatures.push(signature); 128 | } 129 | _ => { 130 | log::warn!("Warning: don't know what to do with sig {:#?}", sig); 131 | } 132 | } 133 | } 134 | 135 | Ok(Interface { 136 | name: name.into(), 137 | functions: signatures, 138 | }) 139 | } 140 | 141 | /// Try to parse the imports 142 | fn parse_imports(tsmod: &TsModuleDecl) -> Result> { 143 | if let Some(block) = &tsmod.body { 144 | if let Some(block) = block.clone().ts_module_block() { 145 | for inter in block.body { 146 | if let ModuleItem::Stmt(Stmt::Decl(decl)) = inter { 147 | let i = decl.as_ts_interface().unwrap(); 148 | let mut interface = parse_user_interface(i)?; 149 | if tsmod.id.clone().str().is_some() { 150 | interface.name = tsmod.id.clone().expect_str().value.as_str().to_string() 151 | + "/" 152 | + i.id.sym.as_str(); 153 | } 154 | return Ok(Some(interface)); 155 | } else { 156 | log::warn!("Not a module decl"); 157 | } 158 | } 159 | } else { 160 | log::warn!("Not a Module Block"); 161 | } 162 | } 163 | Ok(None) 164 | } 165 | 166 | /// Parses the main module declaration (the extism exports) 167 | fn parse_module_decl(tsmod: &TsModuleDecl) -> Result { 168 | let mut signatures = Vec::new(); 169 | 170 | if let Some(block) = &tsmod.body { 171 | if let Some(block) = block.as_ts_module_block() { 172 | for decl in &block.body { 173 | if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = decl { 174 | if let Some(fndecl) = e.decl.as_fn_decl() { 175 | let name = fndecl.ident.sym.as_str().to_string(); 176 | let mut params = vec![]; 177 | let mut results = vec![]; 178 | if let Some(return_type) = fndecl.function.clone().return_type.clone() { 179 | result_type(&mut results, &return_type.type_ann)?; 180 | } 181 | 182 | for param in fndecl.function.params.iter() { 183 | let name = param.pat.clone().expect_ident().id.sym.as_str().to_string(); 184 | let p = param.pat.clone().expect_ident(); 185 | match p.type_ann { 186 | None => params.push(Param::new(&name, val_type("i64")?)), 187 | Some(ann) => { 188 | param_type(&mut params, &name, &ann.type_ann)?; 189 | } 190 | } 191 | } 192 | let signature = Signature { 193 | name, 194 | params, 195 | results, 196 | }; 197 | 198 | signatures.push(signature); 199 | } 200 | } else { 201 | bail!("Don't know what to do with non export on main module"); 202 | } 203 | } 204 | } 205 | } 206 | 207 | Ok(Interface { 208 | name: "main".to_string(), 209 | functions: signatures, 210 | }) 211 | } 212 | 213 | /// Parse the whole TS module type file 214 | fn parse_module(module: Module) -> Result> { 215 | let mut interfaces = Vec::new(); 216 | for statement in &module.body { 217 | if let ModuleItem::Stmt(Stmt::Decl(Decl::TsModule(submod))) = statement { 218 | let name = submod.id.as_str().map(|name| name.value.as_str()); 219 | 220 | match name { 221 | Some("main") | None => { 222 | interfaces.push(parse_module_decl(submod)?); 223 | } 224 | Some(_) => { 225 | if let Some(imports) = parse_imports(submod)? { 226 | interfaces.push(imports); 227 | } 228 | } 229 | }; 230 | } 231 | } 232 | 233 | Ok(interfaces) 234 | } 235 | 236 | /// Parse the d.ts file representing the plugin interface 237 | pub fn parse_interface_file(interface_path: impl AsRef) -> Result { 238 | let cm: Lrc = Default::default(); 239 | let fm = cm.load_file(interface_path.as_ref())?; 240 | let lexer = Lexer::new( 241 | Syntax::Typescript(Default::default()), 242 | Default::default(), 243 | StringInput::from(&*fm), 244 | None, 245 | ); 246 | 247 | let mut parser = Parser::new_from(lexer); 248 | let parse_errs = parser.take_errors(); 249 | if !parse_errs.is_empty() { 250 | for e in parse_errs { 251 | log::error!("{:#?}", e); 252 | } 253 | bail!("Failed to parse TypeScript interface file. It is not valid TypeScript."); 254 | } 255 | 256 | let module = parser.parse_module().expect("failed to parser module"); 257 | let interfaces = parse_module(module)?; 258 | let mut exports = interfaces 259 | .iter() 260 | .find(|i| i.name == "main") 261 | .context("You need to declare a 'main' module")? 262 | .to_owned(); 263 | 264 | exports.functions.sort_by_key(|x| x.name.to_string()); 265 | 266 | let imports = interfaces 267 | .into_iter() 268 | .filter(|i| i.name != "main") 269 | .collect(); 270 | 271 | Ok(PluginInterface { exports, imports }) 272 | } 273 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "js-pdk-core" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | extism-pdk = "1.3" 12 | once_cell = "1.16" 13 | anyhow = { workspace = true } 14 | quickjs-wasm-rs = "3" 15 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 16 | rquickjs = { version = "0.8", features = ["array-buffer", "bindgen"]} 17 | base64 = "0.22.1" 18 | 19 | [lib] 20 | crate-type = ["cdylib"] 21 | -------------------------------------------------------------------------------- /crates/core/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rerun-if-changed=src/prelude/dist/index.js"); 3 | } 4 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rquickjs::{ 2 | function::Args, object::ObjectKeysIter, Context, Ctx, Function, Object, Runtime, Undefined, 3 | Value, 4 | }; 5 | use std::io; 6 | use std::io::Read; 7 | 8 | mod globals; 9 | 10 | struct Cx(Context); 11 | 12 | unsafe impl Send for Cx {} 13 | unsafe impl Sync for Cx {} 14 | 15 | static CONTEXT: std::sync::OnceLock = std::sync::OnceLock::new(); 16 | static CALL_ARGS: std::sync::Mutex>> = std::sync::Mutex::new(vec![]); 17 | 18 | fn check_exception(this: &Ctx, err: rquickjs::Error) -> anyhow::Error { 19 | let s = match err { 20 | rquickjs::Error::Exception => { 21 | let err = this.catch().into_exception().unwrap(); 22 | let msg = err.message().unwrap_or_default(); 23 | format!("Exception: {}\n{}", msg, err.stack().unwrap_or_default()) 24 | } 25 | err => err.to_string(), 26 | }; 27 | 28 | let mem = extism_pdk::Memory::from_bytes(&s).unwrap(); 29 | unsafe { 30 | extism_pdk::extism::error_set(mem.offset()); 31 | } 32 | anyhow::Error::msg(s) 33 | } 34 | 35 | #[export_name = "wizer.initialize"] 36 | extern "C" fn init() { 37 | let runtime = Runtime::new().expect("Couldn't make a runtime"); 38 | let context = Context::full(&runtime).expect("Couldnt make a context"); 39 | globals::inject_globals(&context).expect("Failed to initialize globals"); 40 | 41 | let mut code = String::new(); 42 | io::stdin().read_to_string(&mut code).unwrap(); 43 | 44 | context 45 | .with(|this| -> Result { 46 | match this.eval(code) { 47 | Ok(()) => (), 48 | Err(err) => return Err(check_exception(&this, err)), 49 | } 50 | 51 | Ok(Undefined) 52 | }) 53 | .unwrap(); 54 | let _ = CONTEXT.set(Cx(context)); 55 | } 56 | 57 | fn js_context() -> Context { 58 | if CONTEXT.get().is_none() { 59 | init() 60 | } 61 | let context = CONTEXT.get().unwrap(); 62 | context.0.clone() 63 | } 64 | 65 | fn invoke<'a, T, F: for<'b> Fn(Ctx<'b>, Value<'b>) -> T>( 66 | idx: i32, 67 | conv: F, 68 | ) -> Result { 69 | let call_args = CALL_ARGS.lock().unwrap().pop(); 70 | let context = js_context(); 71 | context.with(|ctx| { 72 | let call_args = call_args.unwrap(); 73 | let args: Args = call_args.iter().fold( 74 | Args::new(ctx.clone(), call_args.len()), 75 | |mut args, rust_arg| { 76 | match rust_arg { 77 | ArgType::I32(v) => args 78 | .push_arg(v) 79 | .expect("Should be able to convert i32 to JS arg"), 80 | ArgType::I64(v) => args 81 | .push_arg(rquickjs::BigInt::from_i64(ctx.clone(), *v)) 82 | .expect("Should be able to convert i64 to JS arg"), 83 | ArgType::F32(v) => args 84 | .push_arg(v) 85 | .expect("Should be able to convert f32 to JS arg"), 86 | ArgType::F64(v) => args 87 | .push_arg(v) 88 | .expect("Should be able to convert f64 to JS arg"), 89 | } 90 | args 91 | }, 92 | ); 93 | let global = ctx.globals(); 94 | 95 | let module: Object = global.get("module")?; 96 | let exports: Object = module.get("exports")?; 97 | 98 | let export_names = export_names(exports.clone()).unwrap(); 99 | 100 | let function: Function = exports.get(export_names[idx as usize].as_str()).unwrap(); 101 | 102 | let function_invocation_result = function.call_arg(args); 103 | 104 | while ctx.execute_pending_job() { 105 | continue 106 | } 107 | 108 | match function_invocation_result { 109 | Ok(r) => { 110 | let res = conv(ctx.clone(), r); 111 | Ok(res) 112 | } 113 | Err(err) => Err(check_exception(&ctx, err)), 114 | } 115 | }) 116 | } 117 | 118 | #[no_mangle] 119 | pub extern "C" fn __arg_start() { 120 | CALL_ARGS.lock().unwrap().push(vec![]); 121 | } 122 | 123 | #[no_mangle] 124 | pub extern "C" fn __arg_i32(arg: i32) { 125 | CALL_ARGS 126 | .lock() 127 | .unwrap() 128 | .last_mut() 129 | .unwrap() 130 | .push(ArgType::I32(arg)); 131 | } 132 | 133 | #[no_mangle] 134 | pub extern "C" fn __arg_i64(arg: i64) { 135 | CALL_ARGS 136 | .lock() 137 | .unwrap() 138 | .last_mut() 139 | .unwrap() 140 | .push(ArgType::I64(arg)); 141 | } 142 | 143 | #[no_mangle] 144 | pub extern "C" fn __arg_f32(arg: f32) { 145 | CALL_ARGS 146 | .lock() 147 | .unwrap() 148 | .last_mut() 149 | .unwrap() 150 | .push(ArgType::F32(arg)); 151 | } 152 | 153 | #[no_mangle] 154 | pub extern "C" fn __arg_f64(arg: f64) { 155 | CALL_ARGS 156 | .lock() 157 | .unwrap() 158 | .last_mut() 159 | .unwrap() 160 | .push(ArgType::F64(arg)); 161 | } 162 | 163 | #[no_mangle] 164 | pub extern "C" fn __invoke_i32(idx: i32) -> i32 { 165 | invoke(idx, |_ctx, r| r.as_number().unwrap_or_default() as i32).unwrap_or(-1) 166 | } 167 | 168 | #[no_mangle] 169 | pub extern "C" fn __invoke_i64(idx: i32) -> i64 { 170 | invoke(idx, |_ctx, r| { 171 | if let Some(number) = r.as_big_int() { 172 | return number.clone().to_i64().unwrap_or_default(); 173 | } else if let Some(number) = r.as_number() { 174 | return number as i64; 175 | } 176 | 0 177 | }) 178 | .unwrap_or(-1) 179 | } 180 | 181 | #[no_mangle] 182 | pub extern "C" fn __invoke_f64(idx: i32) -> f64 { 183 | invoke(idx, |_ctx, r| r.as_float().unwrap_or_default()).unwrap_or(-1.0) 184 | } 185 | 186 | #[no_mangle] 187 | pub extern "C" fn __invoke_f32(idx: i32) -> f32 { 188 | invoke(idx, |_ctx, r| r.as_number().unwrap_or_default() as f32).unwrap_or(-1.0) 189 | } 190 | 191 | #[no_mangle] 192 | pub extern "C" fn __invoke(idx: i32) { 193 | invoke(idx, |_ctx, _r| ()).unwrap() 194 | } 195 | 196 | fn export_names(exports: Object) -> anyhow::Result> { 197 | let mut keys_iter: ObjectKeysIter = exports.keys(); 198 | let mut key = keys_iter.next(); 199 | let mut keys: Vec = vec![]; 200 | while key.is_some() { 201 | keys.push(key.unwrap()?.to_string()); 202 | key = keys_iter.next(); 203 | } 204 | keys.sort(); 205 | Ok(keys) 206 | } 207 | 208 | enum ArgType { 209 | I32(i32), 210 | I64(i64), 211 | F32(f32), 212 | F64(f64), 213 | } 214 | -------------------------------------------------------------------------------- /crates/core/src/prelude/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | -------------------------------------------------------------------------------- /crates/core/src/prelude/README.md: -------------------------------------------------------------------------------- 1 | # extism js-pdk types 2 | 3 | TypeScript definitions for the Extism [JS Plugin Development Kit](https://github.com/extism/js-pdk). 4 | 5 | To install these types, add them to your `tsconfig.json`'s `types`: 6 | 7 | ```json 8 | { 9 | "compilerOptions": { 10 | "lib": [], // this ensures unsupported globals aren't suggested 11 | "types": ["@extism/js-pdk"], // while this makes the IDE aware of the ones that are 12 | } 13 | } 14 | ``` 15 | 16 | ## Development 17 | 18 | The JavaScript prelude is defined in `crates/core/prelude`. 19 | 20 | ## License 21 | 22 | BSD-Clause-3 23 | -------------------------------------------------------------------------------- /crates/core/src/prelude/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | esbuild 4 | .build({ 5 | entryPoints: ["src/index.ts"], 6 | outdir: "dist", 7 | bundle: true, 8 | minify: true, 9 | format: "iife", 10 | target: ["es2020"], // don't go over es2020 because quickjs doesn't support it 11 | }); 12 | -------------------------------------------------------------------------------- /crates/core/src/prelude/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extism/js-pdk", 3 | "version": "1.1.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@extism/js-pdk", 9 | "version": "1.1.1", 10 | "license": "BSD-Clause-3", 11 | "dependencies": { 12 | "urlpattern-polyfill": "^8.0.2" 13 | }, 14 | "devDependencies": { 15 | "core-js": "^3.30.2", 16 | "esbuild": "^0.17.19", 17 | "typescript": "^5.4.5" 18 | } 19 | }, 20 | "node_modules/@esbuild/android-arm": { 21 | "version": "0.17.19", 22 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 23 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 24 | "cpu": [ 25 | "arm" 26 | ], 27 | "dev": true, 28 | "optional": true, 29 | "os": [ 30 | "android" 31 | ], 32 | "engines": { 33 | "node": ">=12" 34 | } 35 | }, 36 | "node_modules/@esbuild/android-arm64": { 37 | "version": "0.17.19", 38 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 39 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 40 | "cpu": [ 41 | "arm64" 42 | ], 43 | "dev": true, 44 | "optional": true, 45 | "os": [ 46 | "android" 47 | ], 48 | "engines": { 49 | "node": ">=12" 50 | } 51 | }, 52 | "node_modules/@esbuild/android-x64": { 53 | "version": "0.17.19", 54 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 55 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 56 | "cpu": [ 57 | "x64" 58 | ], 59 | "dev": true, 60 | "optional": true, 61 | "os": [ 62 | "android" 63 | ], 64 | "engines": { 65 | "node": ">=12" 66 | } 67 | }, 68 | "node_modules/@esbuild/darwin-arm64": { 69 | "version": "0.17.19", 70 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 71 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 72 | "cpu": [ 73 | "arm64" 74 | ], 75 | "dev": true, 76 | "optional": true, 77 | "os": [ 78 | "darwin" 79 | ], 80 | "engines": { 81 | "node": ">=12" 82 | } 83 | }, 84 | "node_modules/@esbuild/darwin-x64": { 85 | "version": "0.17.19", 86 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 87 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 88 | "cpu": [ 89 | "x64" 90 | ], 91 | "dev": true, 92 | "optional": true, 93 | "os": [ 94 | "darwin" 95 | ], 96 | "engines": { 97 | "node": ">=12" 98 | } 99 | }, 100 | "node_modules/@esbuild/freebsd-arm64": { 101 | "version": "0.17.19", 102 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 103 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 104 | "cpu": [ 105 | "arm64" 106 | ], 107 | "dev": true, 108 | "optional": true, 109 | "os": [ 110 | "freebsd" 111 | ], 112 | "engines": { 113 | "node": ">=12" 114 | } 115 | }, 116 | "node_modules/@esbuild/freebsd-x64": { 117 | "version": "0.17.19", 118 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 119 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 120 | "cpu": [ 121 | "x64" 122 | ], 123 | "dev": true, 124 | "optional": true, 125 | "os": [ 126 | "freebsd" 127 | ], 128 | "engines": { 129 | "node": ">=12" 130 | } 131 | }, 132 | "node_modules/@esbuild/linux-arm": { 133 | "version": "0.17.19", 134 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 135 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 136 | "cpu": [ 137 | "arm" 138 | ], 139 | "dev": true, 140 | "optional": true, 141 | "os": [ 142 | "linux" 143 | ], 144 | "engines": { 145 | "node": ">=12" 146 | } 147 | }, 148 | "node_modules/@esbuild/linux-arm64": { 149 | "version": "0.17.19", 150 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 151 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 152 | "cpu": [ 153 | "arm64" 154 | ], 155 | "dev": true, 156 | "optional": true, 157 | "os": [ 158 | "linux" 159 | ], 160 | "engines": { 161 | "node": ">=12" 162 | } 163 | }, 164 | "node_modules/@esbuild/linux-ia32": { 165 | "version": "0.17.19", 166 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 167 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 168 | "cpu": [ 169 | "ia32" 170 | ], 171 | "dev": true, 172 | "optional": true, 173 | "os": [ 174 | "linux" 175 | ], 176 | "engines": { 177 | "node": ">=12" 178 | } 179 | }, 180 | "node_modules/@esbuild/linux-loong64": { 181 | "version": "0.17.19", 182 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 183 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 184 | "cpu": [ 185 | "loong64" 186 | ], 187 | "dev": true, 188 | "optional": true, 189 | "os": [ 190 | "linux" 191 | ], 192 | "engines": { 193 | "node": ">=12" 194 | } 195 | }, 196 | "node_modules/@esbuild/linux-mips64el": { 197 | "version": "0.17.19", 198 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 199 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 200 | "cpu": [ 201 | "mips64el" 202 | ], 203 | "dev": true, 204 | "optional": true, 205 | "os": [ 206 | "linux" 207 | ], 208 | "engines": { 209 | "node": ">=12" 210 | } 211 | }, 212 | "node_modules/@esbuild/linux-ppc64": { 213 | "version": "0.17.19", 214 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 215 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 216 | "cpu": [ 217 | "ppc64" 218 | ], 219 | "dev": true, 220 | "optional": true, 221 | "os": [ 222 | "linux" 223 | ], 224 | "engines": { 225 | "node": ">=12" 226 | } 227 | }, 228 | "node_modules/@esbuild/linux-riscv64": { 229 | "version": "0.17.19", 230 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 231 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 232 | "cpu": [ 233 | "riscv64" 234 | ], 235 | "dev": true, 236 | "optional": true, 237 | "os": [ 238 | "linux" 239 | ], 240 | "engines": { 241 | "node": ">=12" 242 | } 243 | }, 244 | "node_modules/@esbuild/linux-s390x": { 245 | "version": "0.17.19", 246 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 247 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 248 | "cpu": [ 249 | "s390x" 250 | ], 251 | "dev": true, 252 | "optional": true, 253 | "os": [ 254 | "linux" 255 | ], 256 | "engines": { 257 | "node": ">=12" 258 | } 259 | }, 260 | "node_modules/@esbuild/linux-x64": { 261 | "version": "0.17.19", 262 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 263 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 264 | "cpu": [ 265 | "x64" 266 | ], 267 | "dev": true, 268 | "optional": true, 269 | "os": [ 270 | "linux" 271 | ], 272 | "engines": { 273 | "node": ">=12" 274 | } 275 | }, 276 | "node_modules/@esbuild/netbsd-x64": { 277 | "version": "0.17.19", 278 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 279 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 280 | "cpu": [ 281 | "x64" 282 | ], 283 | "dev": true, 284 | "optional": true, 285 | "os": [ 286 | "netbsd" 287 | ], 288 | "engines": { 289 | "node": ">=12" 290 | } 291 | }, 292 | "node_modules/@esbuild/openbsd-x64": { 293 | "version": "0.17.19", 294 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 295 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 296 | "cpu": [ 297 | "x64" 298 | ], 299 | "dev": true, 300 | "optional": true, 301 | "os": [ 302 | "openbsd" 303 | ], 304 | "engines": { 305 | "node": ">=12" 306 | } 307 | }, 308 | "node_modules/@esbuild/sunos-x64": { 309 | "version": "0.17.19", 310 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 311 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 312 | "cpu": [ 313 | "x64" 314 | ], 315 | "dev": true, 316 | "optional": true, 317 | "os": [ 318 | "sunos" 319 | ], 320 | "engines": { 321 | "node": ">=12" 322 | } 323 | }, 324 | "node_modules/@esbuild/win32-arm64": { 325 | "version": "0.17.19", 326 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 327 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 328 | "cpu": [ 329 | "arm64" 330 | ], 331 | "dev": true, 332 | "optional": true, 333 | "os": [ 334 | "win32" 335 | ], 336 | "engines": { 337 | "node": ">=12" 338 | } 339 | }, 340 | "node_modules/@esbuild/win32-ia32": { 341 | "version": "0.17.19", 342 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 343 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 344 | "cpu": [ 345 | "ia32" 346 | ], 347 | "dev": true, 348 | "optional": true, 349 | "os": [ 350 | "win32" 351 | ], 352 | "engines": { 353 | "node": ">=12" 354 | } 355 | }, 356 | "node_modules/@esbuild/win32-x64": { 357 | "version": "0.17.19", 358 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 359 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 360 | "cpu": [ 361 | "x64" 362 | ], 363 | "dev": true, 364 | "optional": true, 365 | "os": [ 366 | "win32" 367 | ], 368 | "engines": { 369 | "node": ">=12" 370 | } 371 | }, 372 | "node_modules/core-js": { 373 | "version": "3.30.2", 374 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", 375 | "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==", 376 | "dev": true, 377 | "hasInstallScript": true, 378 | "funding": { 379 | "type": "opencollective", 380 | "url": "https://opencollective.com/core-js" 381 | } 382 | }, 383 | "node_modules/esbuild": { 384 | "version": "0.17.19", 385 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 386 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 387 | "dev": true, 388 | "hasInstallScript": true, 389 | "bin": { 390 | "esbuild": "bin/esbuild" 391 | }, 392 | "engines": { 393 | "node": ">=12" 394 | }, 395 | "optionalDependencies": { 396 | "@esbuild/android-arm": "0.17.19", 397 | "@esbuild/android-arm64": "0.17.19", 398 | "@esbuild/android-x64": "0.17.19", 399 | "@esbuild/darwin-arm64": "0.17.19", 400 | "@esbuild/darwin-x64": "0.17.19", 401 | "@esbuild/freebsd-arm64": "0.17.19", 402 | "@esbuild/freebsd-x64": "0.17.19", 403 | "@esbuild/linux-arm": "0.17.19", 404 | "@esbuild/linux-arm64": "0.17.19", 405 | "@esbuild/linux-ia32": "0.17.19", 406 | "@esbuild/linux-loong64": "0.17.19", 407 | "@esbuild/linux-mips64el": "0.17.19", 408 | "@esbuild/linux-ppc64": "0.17.19", 409 | "@esbuild/linux-riscv64": "0.17.19", 410 | "@esbuild/linux-s390x": "0.17.19", 411 | "@esbuild/linux-x64": "0.17.19", 412 | "@esbuild/netbsd-x64": "0.17.19", 413 | "@esbuild/openbsd-x64": "0.17.19", 414 | "@esbuild/sunos-x64": "0.17.19", 415 | "@esbuild/win32-arm64": "0.17.19", 416 | "@esbuild/win32-ia32": "0.17.19", 417 | "@esbuild/win32-x64": "0.17.19" 418 | } 419 | }, 420 | "node_modules/typescript": { 421 | "version": "5.4.5", 422 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 423 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 424 | "dev": true, 425 | "bin": { 426 | "tsc": "bin/tsc", 427 | "tsserver": "bin/tsserver" 428 | }, 429 | "engines": { 430 | "node": ">=14.17" 431 | } 432 | }, 433 | "node_modules/urlpattern-polyfill": { 434 | "version": "8.0.2", 435 | "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", 436 | "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==" 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /crates/core/src/prelude/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@extism/js-pdk", 3 | "version": "1.1.1", 4 | "description": "typescript definitions for the extism js-pdk", 5 | "types": "dist/index.d.ts", 6 | "files": ["dist/*.d.ts", "types/*.d.ts"], 7 | "scripts": { 8 | "build": "node esbuild.js && tsc" 9 | }, 10 | "author": "The Extism Authors ", 11 | "license": "BSD-Clause-3", 12 | "dependencies": { 13 | "urlpattern-polyfill": "^8.0.2" 14 | }, 15 | "devDependencies": { 16 | "core-js": "^3.30.2", 17 | "esbuild": "^0.17.19", 18 | "typescript": "^5.4.5" 19 | }, 20 | "exports": { 21 | ".": { 22 | "default": null, 23 | "types": "./dist/index.d.ts" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/config.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | var Config: { 3 | get(key: string): string | null; 4 | }; 5 | } 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/console.ts: -------------------------------------------------------------------------------- 1 | 2 | declare global { 3 | interface Console { 4 | debug(...data: any[]): void; 5 | error(...data: any[]): void; 6 | info(...data: any[]): void; 7 | log(...data: any[]): void; 8 | warn(...data: any[]): void; 9 | } 10 | 11 | /** 12 | * @internal 13 | */ 14 | var __consoleWrite: (level: string, message: string) => void; 15 | } 16 | 17 | function stringifyArg(arg: any): string { 18 | if (arg === null) return 'null'; 19 | if (arg === undefined) return 'undefined'; 20 | 21 | if (typeof arg === 'symbol') return arg.toString(); 22 | if (typeof arg === 'bigint') return `${arg}n`; 23 | if (typeof arg === 'function') return `[Function ${arg.name ? `${arg.name}` : '(anonymous)'}]`; 24 | 25 | if (typeof arg === 'object') { 26 | if (arg instanceof Error) { 27 | return `${arg.name}: ${arg.message}${arg.stack ? '\n' : ''}${arg.stack}`; 28 | } 29 | if (arg instanceof Set) { 30 | return `Set(${arg.size}) { ${Array.from(arg).map(String).join(', ')} }`; 31 | } 32 | if (arg instanceof Map) { 33 | return `Map(${arg.size}) { ${Array.from(arg).map(([k, v]) => `${k} => ${v}`).join(', ')} }`; 34 | } 35 | if (Array.isArray(arg)) { 36 | const items = []; 37 | for (let i = 0; i < arg.length; i++) { 38 | items.push(i in arg ? stringifyArg(arg[i]) : ''); 39 | } 40 | return `[ ${items.join(', ')} ]`; 41 | } 42 | 43 | // For regular objects, use JSON.stringify first for clean output 44 | try { 45 | return JSON.stringify(arg); 46 | } catch { 47 | // For objects that can't be JSON stringified (circular refs etc) 48 | // fall back to Object.prototype.toString behavior 49 | return Object.prototype.toString.call(arg); 50 | } 51 | } 52 | 53 | return String(arg); 54 | } 55 | 56 | function createLogFunction(level: string) { 57 | return function (...args: any[]) { 58 | const message = args.map(stringifyArg).join(' '); 59 | __consoleWrite(level, message); 60 | }; 61 | } 62 | 63 | const console = { 64 | trace: createLogFunction('trace'), 65 | debug: createLogFunction('debug'), 66 | log: createLogFunction('info'), 67 | info: createLogFunction('info'), 68 | warn: createLogFunction('warn'), 69 | error: createLogFunction('error'), 70 | }; 71 | 72 | globalThis.console = console; 73 | 74 | export { }; 75 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/date.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | /** 3 | * @internal 4 | */ 5 | function __getTime(): string; 6 | } 7 | 8 | globalThis.Date = new Proxy(Date, { 9 | apply() { 10 | return __getTime(); 11 | }, 12 | construct(target, args) { 13 | if (args.length === 0) return new target(__getTime()); 14 | 15 | return Reflect.construct(target, args); 16 | }, 17 | }); 18 | 19 | export {}; 20 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/host.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Host { 3 | /** 4 | * @internal 5 | */ 6 | __hostFunctions: Array<{ name: string; results: number }>; 7 | 8 | invokeFunc(id: number, ...args: unknown[]): number; 9 | inputBytes(): ArrayBufferLike; 10 | inputString(): string; 11 | outputBytes(output: ArrayBufferLike): boolean; 12 | outputString(output: string): boolean; 13 | getFunctions(): import("extism:host").user; 14 | arrayBufferToBase64(input: ArrayBuffer): string; 15 | base64ToArrayBuffer(input: string): ArrayBuffer; 16 | } 17 | 18 | var Host: Host; 19 | } 20 | 21 | Host.getFunctions = function () { 22 | return Host.__hostFunctions.reduce((funcs, meta, id) => { 23 | funcs[meta.name] = (...args: unknown[]) => { 24 | const sanitizedArgs = args.map(arg => 25 | arg === undefined || arg === null ? 0 : arg 26 | ); 27 | const result = Host.invokeFunc(id, ...sanitizedArgs); 28 | return meta.results === 0 ? undefined : result; 29 | }; 30 | return funcs; 31 | }, {}); 32 | }; 33 | 34 | export { }; -------------------------------------------------------------------------------- /crates/core/src/prelude/src/http.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface HttpRequest { 3 | url: string; 4 | // method defaults to "GET" if not provided 5 | method?: 6 | | "GET" 7 | | "HEAD" 8 | | "POST" 9 | | "PUT" 10 | | "DELETE" 11 | | "CONNECT" 12 | | "OPTIONS" 13 | | "TRACE" 14 | | "PATCH"; 15 | headers?: Record; 16 | } 17 | 18 | interface HttpResponse { 19 | body: string; 20 | status: number; 21 | } 22 | 23 | var Http: { 24 | request(req: HttpRequest, body?: string | ArrayBufferLike): HttpResponse; 25 | }; 26 | } 27 | 28 | Http.request = new Proxy(Http.request, { 29 | apply(target, thisArg, [req, body]) { 30 | // convert body to string if it's an arraybuffer 31 | if (typeof body === "object" && "byteLength" in body) { 32 | body = new Uint8Array(body).toString(); 33 | } 34 | 35 | if (req.method === undefined) { 36 | req.method = "GET"; 37 | } 38 | 39 | return Reflect.apply( 40 | target, 41 | thisArg, 42 | // TODO: We need to completely avoid passing a second argument due to a bug in the runtime, 43 | // which converts `undefined` to `"undefined"`. This is also the case for req.method. 44 | body !== undefined ? [req, body] : [req], 45 | ); 46 | }, 47 | }); 48 | 49 | export {}; 50 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | import "core-js/actual/url"; 7 | import "core-js/actual/url/to-json"; 8 | import "core-js/actual/url-search-params"; 9 | import "urlpattern-polyfill"; 10 | 11 | import "./config"; 12 | import "./date"; 13 | import "./text-decoder"; 14 | import "./text-encoder"; 15 | import "./host"; 16 | import "./http"; 17 | import "./memory"; 18 | import "./memory-handle"; 19 | import "./var"; 20 | import "./console"; -------------------------------------------------------------------------------- /crates/core/src/prelude/src/memory-handle.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface MemoryHandle { 3 | offset: PTR; 4 | len: I64; 5 | 6 | readString(): string; 7 | readUInt32(): number; 8 | readUInt64(): bigint; 9 | readFloat32(): number; 10 | readUFloat64(): number; 11 | readBytes(): ArrayBuffer; 12 | readJsonObject(): T; 13 | free(): void; 14 | } 15 | 16 | var MemoryHandle: { 17 | prototype: MemoryHandle; 18 | new(offset: PTR, len: I64): MemoryHandle; 19 | }; 20 | } 21 | 22 | class MemoryHandle implements globalThis.MemoryHandle { 23 | offset: PTR; 24 | len: I64; 25 | 26 | constructor(offset: PTR, len: I64) { 27 | this.offset = offset; 28 | this.len = len; 29 | } 30 | 31 | readString() { 32 | return new TextDecoder().decode(this.readBytes()); 33 | } 34 | 35 | readUInt32() { 36 | const bytes = this.readBytes(); 37 | const arr = new Uint32Array(bytes); 38 | return arr[0]; 39 | } 40 | 41 | readUInt64() { 42 | const bytes = this.readBytes(); 43 | const arr = new BigUint64Array(bytes); 44 | return arr[0]; 45 | } 46 | 47 | readFloat32() { 48 | const bytes = this.readBytes(); 49 | const arr = new Float32Array(bytes); 50 | return arr[0]; 51 | } 52 | 53 | readUFloat64() { 54 | const bytes = this.readBytes(); 55 | const arr = new Float64Array(bytes); 56 | return arr[0]; 57 | } 58 | 59 | readBytes() { 60 | return Memory._readBytes(this.offset); 61 | } 62 | 63 | readJsonObject() { 64 | return JSON.parse(this.readString()); 65 | } 66 | 67 | free() { 68 | Memory._free(this.offset); 69 | } 70 | } 71 | 72 | globalThis.MemoryHandle = MemoryHandle; 73 | 74 | export { }; 75 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/memory.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Memory { 3 | fromString(str: string): MemoryHandle; 4 | fromBuffer(bytes: ArrayBufferLike): MemoryHandle; 5 | fromJsonObject(obj: unknown): MemoryHandle; 6 | allocUInt32(i: number): MemoryHandle; 7 | allocUInt64(i: bigint): MemoryHandle; 8 | allocFloat32(i: number): MemoryHandle; 9 | allocFloat64(i: number): MemoryHandle; 10 | find(offset: PTR): MemoryHandle; 11 | 12 | /** 13 | * @internal 14 | */ 15 | _fromBuffer(buffer: ArrayBuffer): { offset: PTR; len: I64 }; 16 | /** 17 | * @internal 18 | */ 19 | _find(offset: PTR): { offset: PTR; len: I64 }; 20 | /** 21 | * @internal 22 | */ 23 | _free(offset: PTR): void; 24 | /** 25 | * @internal 26 | */ 27 | _readBytes(offset: PTR): ArrayBuffer; 28 | } 29 | var Memory: Memory; 30 | } 31 | 32 | const Memory = globalThis.Memory; 33 | Memory.fromString = function(this: Memory, str) { 34 | let bytes = new TextEncoder().encode(str).buffer; 35 | const memData = Memory.fromBuffer(bytes); 36 | return new MemoryHandle(memData.offset, memData.len); 37 | }; 38 | 39 | Memory.fromBuffer = function(this: Memory, bytes) { 40 | const memData = Memory._fromBuffer(bytes); 41 | return new MemoryHandle(memData.offset, memData.len); 42 | }; 43 | 44 | Memory.fromJsonObject = function(this: Memory, obj) { 45 | const memData = Memory.fromString(JSON.stringify(obj)); 46 | return new MemoryHandle(memData.offset, memData.len); 47 | }; 48 | 49 | Memory.allocUInt32 = function(this: Memory, i) { 50 | const buffer = new ArrayBuffer(4); 51 | const arr = new Uint32Array(buffer); 52 | arr[0] = i; 53 | return Memory.fromBuffer(buffer); 54 | }; 55 | 56 | Memory.allocUInt64 = function(this: Memory, i) { 57 | const buffer = new ArrayBuffer(8); 58 | const arr = new BigUint64Array(buffer); 59 | arr[0] = i; 60 | return Memory.fromBuffer(buffer); 61 | }; 62 | 63 | Memory.allocFloat32 = function(this: Memory, i) { 64 | const buffer = new ArrayBuffer(4); 65 | const arr = new Float32Array(buffer); 66 | arr[0] = i; 67 | return Memory.fromBuffer(buffer); 68 | }; 69 | 70 | Memory.allocFloat64 = function(this: Memory, i) { 71 | const buffer = new ArrayBuffer(8); 72 | const arr = new Float64Array(buffer); 73 | arr[0] = i; 74 | return Memory.fromBuffer(buffer); 75 | }; 76 | 77 | Memory.find = function(offset) { 78 | const memData = Memory._find(offset); 79 | if (memData === undefined) { 80 | return undefined; 81 | } 82 | return new MemoryHandle(memData.offset, memData.len); 83 | }; 84 | 85 | export { }; 86 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/text-decoder.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | /** 3 | * @internal 4 | */ 5 | function __decodeUtf8BufferToString( 6 | input: ArrayBufferLike, 7 | byteOffset: number, 8 | byteLength: number, 9 | fatal: boolean, 10 | ignoreBOM: boolean 11 | ): string; 12 | } 13 | 14 | class TextDecoder implements globalThis.TextDecoder { 15 | readonly encoding: string; 16 | readonly fatal: boolean; 17 | readonly ignoreBOM: boolean; 18 | 19 | constructor(label: string = "utf-8", options: TextDecoderOptions = {}) { 20 | label = label.trim().toLowerCase(); 21 | const acceptedLabels = [ 22 | "utf-8", 23 | "utf8", 24 | "unicode-1-1-utf-8", 25 | "unicode11utf8", 26 | "unicode20utf8", 27 | "x-unicode20utf8", 28 | ]; 29 | if (!acceptedLabels.includes(label)) { 30 | // Not spec-compliant behaviour 31 | throw new RangeError("The encoding label provided must be utf-8"); 32 | } 33 | 34 | this.encoding = "utf-8"; 35 | this.fatal = !!options.fatal; 36 | this.ignoreBOM = !!options.ignoreBOM; 37 | } 38 | 39 | decode( 40 | input?: AllowSharedBufferSource, 41 | options: TextDecodeOptions = {} 42 | ): string { 43 | if (input === undefined) { 44 | return ""; 45 | } 46 | 47 | if (options.stream) { 48 | throw new Error("Streaming decode is not supported"); 49 | } 50 | 51 | // backing buffer would not have byteOffset and may have different byteLength 52 | let byteOffset = 0; 53 | let byteLength = input.byteLength; 54 | if (ArrayBuffer.isView(input)) { 55 | byteOffset = input.byteOffset; 56 | input = input.buffer; 57 | } 58 | 59 | if (!(input instanceof ArrayBuffer)) { 60 | throw new TypeError( 61 | "The provided value is not of type '(ArrayBuffer or ArrayBufferView)'" 62 | ); 63 | } 64 | 65 | return __decodeUtf8BufferToString( 66 | input, 67 | byteOffset, 68 | byteLength, 69 | this.fatal, 70 | this.ignoreBOM 71 | ); 72 | } 73 | } 74 | 75 | globalThis.TextDecoder = TextDecoder; 76 | 77 | export {}; 78 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/text-encoder.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | /** 3 | * @internal 4 | */ 5 | function __encodeStringToUtf8Buffer(input: string): ArrayBufferLike; 6 | } 7 | 8 | class TextEncoder implements globalThis.TextEncoder { 9 | readonly encoding: string; 10 | 11 | constructor() { 12 | this.encoding = "utf-8"; 13 | } 14 | 15 | encode(input: string = ""): Uint8Array { 16 | input = input.toString(); // non-string inputs are converted to strings 17 | return new Uint8Array(__encodeStringToUtf8Buffer(input)); 18 | } 19 | 20 | encodeInto( 21 | _source: string, 22 | _destination: Uint8Array 23 | ): TextEncoderEncodeIntoResult { 24 | throw new Error("encodeInto is not supported"); 25 | } 26 | } 27 | 28 | globalThis.TextEncoder = TextEncoder; 29 | 30 | export {}; 31 | -------------------------------------------------------------------------------- /crates/core/src/prelude/src/var.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | var Var: { 3 | set(name: string, value: string | ArrayBufferLike): void; 4 | getBytes(name: string): ArrayBufferLike | null; 5 | getString(name: string): string | null; 6 | }; 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /crates/core/src/prelude/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outFile": "dist/index.d.ts", 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "stripInternal": true, 7 | "lib": [], 8 | "types": [] 9 | }, 10 | "include": ["src/**/*", "types/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /crates/core/src/prelude/types/plugin-interface.d.ts: -------------------------------------------------------------------------------- 1 | declare type I32 = number; 2 | declare type I64 = number | bigint; 3 | declare type F32 = number; 4 | declare type F64 = number; 5 | declare type PTR = I64; 6 | 7 | declare module "extism:host" { 8 | interface user { } 9 | } 10 | 11 | declare module "main" { } 12 | -------------------------------------------------------------------------------- /crates/core/src/prelude/types/polyfills.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 9 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 10 | MERCHANTABLITY OR NON-INFRINGEMENT. 11 | 12 | See the Apache Version 2.0 License for specific language governing permissions 13 | and limitations under the License. 14 | ***************************************************************************** */ 15 | 16 | /// 17 | 18 | /** 19 | * The URL interface represents an object providing static methods used for creating object URLs. 20 | * 21 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) 22 | */ 23 | interface URL { 24 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ 25 | hash: string; 26 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ 27 | host: string; 28 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ 29 | hostname: string; 30 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ 31 | href: string; 32 | toString(): string; 33 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) */ 34 | readonly origin: string; 35 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ 36 | password: string; 37 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ 38 | pathname: string; 39 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ 40 | port: string; 41 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ 42 | protocol: string; 43 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ 44 | search: string; 45 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) */ 46 | readonly searchParams: URLSearchParams; 47 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ 48 | username: string; 49 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) */ 50 | toJSON(): string; 51 | } 52 | 53 | declare var URL: { 54 | prototype: URL; 55 | new (url: string | URL, base?: string | URL): URL; 56 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) */ 57 | canParse(url: string | URL, base?: string): boolean; 58 | }; 59 | 60 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) */ 61 | interface URLSearchParams { 62 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) */ 63 | readonly size: number; 64 | /** 65 | * Appends a specified key/value pair as a new search parameter. 66 | * 67 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) 68 | */ 69 | append(name: string, value: string): void; 70 | /** 71 | * Deletes the given search parameter, and its associated value, from the list of all search parameters. 72 | * 73 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) 74 | */ 75 | delete(name: string, value?: string): void; 76 | /** 77 | * Returns the first value associated to the given search parameter. 78 | * 79 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) 80 | */ 81 | get(name: string): string | null; 82 | /** 83 | * Returns all the values association with a given search parameter. 84 | * 85 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) 86 | */ 87 | getAll(name: string): string[]; 88 | /** 89 | * Returns a Boolean indicating if such a search parameter exists. 90 | * 91 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) 92 | */ 93 | has(name: string, value?: string): boolean; 94 | /** 95 | * Sets the value associated to a given search parameter to the given value. If there were several values, delete the others. 96 | * 97 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) 98 | */ 99 | set(name: string, value: string): void; 100 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) */ 101 | sort(): void; 102 | /** Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ 103 | toString(): string; 104 | forEach( 105 | callbackfn: (value: string, key: string, parent: URLSearchParams) => void, 106 | thisArg?: any 107 | ): void; 108 | } 109 | 110 | declare var URLSearchParams: { 111 | prototype: URLSearchParams; 112 | new ( 113 | init?: string[][] | Record | string | URLSearchParams 114 | ): URLSearchParams; 115 | }; 116 | 117 | interface TextDecodeOptions { 118 | stream?: boolean; 119 | } 120 | 121 | interface TextDecoderOptions { 122 | fatal?: boolean; 123 | ignoreBOM?: boolean; 124 | } 125 | 126 | interface TextEncoderEncodeIntoResult { 127 | read: number; 128 | written: number; 129 | } 130 | 131 | type AllowSharedBufferSource = ArrayBuffer | ArrayBufferView; 132 | 133 | /** 134 | * A decoder for a specific method, that is a specific character encoding, like utf-8, iso-8859-2, koi8, cp1261, gbk, etc. A decoder takes a stream of bytes as input and emits a stream of code points. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. 135 | * 136 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) 137 | */ 138 | interface TextDecoder extends TextDecoderCommon { 139 | /** 140 | * Returns the result of running encoding's decoder. The method can be invoked zero or more times with options's stream set to true, and then once without options's stream (or set to false), to process a fragmented input. If the invocation without options's stream (or set to false) has no input, it's clearest to omit both arguments. 141 | * 142 | * ``` 143 | * var string = "", decoder = new TextDecoder(encoding), buffer; 144 | * while(buffer = next_chunk()) { 145 | * string += decoder.decode(buffer, {stream:true}); 146 | * } 147 | * string += decoder.decode(); // end-of-queue 148 | * ``` 149 | * 150 | * If the error mode is "fatal" and encoding's decoder returns error, throws a TypeError. 151 | * 152 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) 153 | */ 154 | decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string; 155 | } 156 | 157 | declare var TextDecoder: { 158 | prototype: TextDecoder; 159 | new (label?: string, options?: TextDecoderOptions): TextDecoder; 160 | }; 161 | 162 | interface TextDecoderCommon { 163 | /** 164 | * Returns encoding's name, lowercased. 165 | * 166 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/encoding) 167 | */ 168 | readonly encoding: string; 169 | /** 170 | * Returns true if error mode is "fatal", otherwise false. 171 | * 172 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/fatal) 173 | */ 174 | readonly fatal: boolean; 175 | /** 176 | * Returns the value of ignore BOM. 177 | * 178 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/ignoreBOM) 179 | */ 180 | readonly ignoreBOM: boolean; 181 | } 182 | 183 | /** 184 | * TextEncoder takes a stream of code points as input and emits a stream of bytes. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. 185 | * 186 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) 187 | */ 188 | interface TextEncoder extends TextEncoderCommon { 189 | /** 190 | * Returns the result of running UTF-8's encoder. 191 | * 192 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) 193 | */ 194 | encode(input?: string): Uint8Array; 195 | /** 196 | * Runs the UTF-8 encoder on source, stores the result of that operation into destination, and returns the progress made as an object wherein read is the number of converted code units of source and written is the number of bytes modified in destination. 197 | * 198 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) 199 | */ 200 | encodeInto( 201 | source: string, 202 | destination: Uint8Array 203 | ): TextEncoderEncodeIntoResult; 204 | } 205 | 206 | declare var TextEncoder: { 207 | prototype: TextEncoder; 208 | new (): TextEncoder; 209 | }; 210 | 211 | interface TextEncoderCommon { 212 | /** 213 | * Returns "utf-8". 214 | * 215 | * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encoding) 216 | */ 217 | readonly encoding: string; 218 | } 219 | 220 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */ 221 | interface Console { 222 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ 223 | debug(...data: any[]): void; 224 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */ 225 | error(...data: any[]): void; 226 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ 227 | info(...data: any[]): void; 228 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */ 229 | log(...data: any[]): void; 230 | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */ 231 | warn(...data: any[]): void; 232 | } 233 | 234 | declare var console: Console; 235 | -------------------------------------------------------------------------------- /example-schema.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://xtp.dylibso.com/assets/wasm/schema.json 2 | # Learn more at https://docs.xtp.dylibso.com/docs/concepts/xtp-schema 3 | version: v1-draft 4 | exports: 5 | CountVowels: 6 | input: 7 | type: string 8 | contentType: text/plain; charset=utf-8 9 | output: 10 | $ref: "#/components/schemas/VowelReport" 11 | contentType: application/json 12 | components: 13 | schemas: 14 | VowelReport: 15 | description: The result of counting vowels on the Vowels input. 16 | properties: 17 | count: 18 | type: integer 19 | format: int32 20 | description: The count of vowels for input string. 21 | total: 22 | type: integer 23 | format: int32 24 | description: The cumulative amount of vowels counted, if this keeps state across multiple function calls. 25 | nullable: true 26 | vowels: 27 | type: string 28 | description: The set of vowels used to get the count, e.g. "aAeEiIoOuU" 29 | -------------------------------------------------------------------------------- /examples/base64/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": [ 5 | "../../crates/core/src/prelude" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/base64/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/base64/script.js: -------------------------------------------------------------------------------- 1 | function greet() { 2 | try { 3 | const base64String = "SGVsbG8g8J+MjSBXb3JsZCHwn4yN"; 4 | 5 | console.log('decoding string:', base64String); 6 | 7 | const decodedBuffer = Host.base64ToArrayBuffer(base64String); 8 | const decodedString = new TextDecoder().decode(decodedBuffer); 9 | 10 | console.log('decoded string:', decodedString); 11 | 12 | const encodedBuffer = Host.arrayBufferToBase64(decodedBuffer); 13 | 14 | console.log('encoded string:', encodedBuffer); 15 | 16 | Host.outputString(`Hello, ${Host.inputString()}`) 17 | } catch (error) { 18 | console.error('Base64 test failed, Error:', JSON.stringify(error)); 19 | } 20 | } 21 | 22 | module.exports = { greet }; 23 | -------------------------------------------------------------------------------- /examples/bundled/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /examples/bundled/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | esbuild 4 | .build({ 5 | entryPoints: ['src/index.ts'], 6 | outdir: 'dist', 7 | bundle: true, 8 | sourcemap: true, 9 | minify: false, // might want to use true for production build 10 | format: 'cjs', // needs to be CJS for now 11 | target: ['es2020'] // don't go over es2020 because quickjs doesn't support it 12 | }) 13 | -------------------------------------------------------------------------------- /examples/bundled/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundled-plugin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "bundled-plugin", 9 | "version": "1.0.0", 10 | "license": "BSD-3-Clause", 11 | "devDependencies": { 12 | "@extism/js-pdk": "../../crates/core/src/prelude", 13 | "cross-env": "^7.0.3", 14 | "esbuild": "^0.19.6", 15 | "typescript": "^5.3.2" 16 | } 17 | }, 18 | "../../crates/core/src/prelude": { 19 | "name": "@extism/js-pdk", 20 | "version": "1.1.1", 21 | "dev": true, 22 | "license": "BSD-Clause-3", 23 | "dependencies": { 24 | "urlpattern-polyfill": "^8.0.2" 25 | }, 26 | "devDependencies": { 27 | "core-js": "^3.30.2", 28 | "esbuild": "^0.17.19", 29 | "typescript": "^5.4.5" 30 | } 31 | }, 32 | "node_modules/@esbuild/android-arm": { 33 | "version": "0.19.6", 34 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.6.tgz", 35 | "integrity": "sha512-muPzBqXJKCbMYoNbb1JpZh/ynl0xS6/+pLjrofcR3Nad82SbsCogYzUE6Aq9QT3cLP0jR/IVK/NHC9b90mSHtg==", 36 | "cpu": [ 37 | "arm" 38 | ], 39 | "dev": true, 40 | "optional": true, 41 | "os": [ 42 | "android" 43 | ], 44 | "engines": { 45 | "node": ">=12" 46 | } 47 | }, 48 | "node_modules/@esbuild/android-arm64": { 49 | "version": "0.19.6", 50 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.6.tgz", 51 | "integrity": "sha512-KQ/hbe9SJvIJ4sR+2PcZ41IBV+LPJyYp6V1K1P1xcMRup9iYsBoQn4MzE3mhMLOld27Au2eDcLlIREeKGUXpHQ==", 52 | "cpu": [ 53 | "arm64" 54 | ], 55 | "dev": true, 56 | "optional": true, 57 | "os": [ 58 | "android" 59 | ], 60 | "engines": { 61 | "node": ">=12" 62 | } 63 | }, 64 | "node_modules/@esbuild/android-x64": { 65 | "version": "0.19.6", 66 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.6.tgz", 67 | "integrity": "sha512-VVJVZQ7p5BBOKoNxd0Ly3xUM78Y4DyOoFKdkdAe2m11jbh0LEU4bPles4e/72EMl4tapko8o915UalN/5zhspg==", 68 | "cpu": [ 69 | "x64" 70 | ], 71 | "dev": true, 72 | "optional": true, 73 | "os": [ 74 | "android" 75 | ], 76 | "engines": { 77 | "node": ">=12" 78 | } 79 | }, 80 | "node_modules/@esbuild/darwin-arm64": { 81 | "version": "0.19.6", 82 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.6.tgz", 83 | "integrity": "sha512-91LoRp/uZAKx6ESNspL3I46ypwzdqyDLXZH7x2QYCLgtnaU08+AXEbabY2yExIz03/am0DivsTtbdxzGejfXpA==", 84 | "cpu": [ 85 | "arm64" 86 | ], 87 | "dev": true, 88 | "optional": true, 89 | "os": [ 90 | "darwin" 91 | ], 92 | "engines": { 93 | "node": ">=12" 94 | } 95 | }, 96 | "node_modules/@esbuild/darwin-x64": { 97 | "version": "0.19.6", 98 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.6.tgz", 99 | "integrity": "sha512-QCGHw770ubjBU1J3ZkFJh671MFajGTYMZumPs9E/rqU52md6lIil97BR0CbPq6U+vTh3xnTNDHKRdR8ggHnmxQ==", 100 | "cpu": [ 101 | "x64" 102 | ], 103 | "dev": true, 104 | "optional": true, 105 | "os": [ 106 | "darwin" 107 | ], 108 | "engines": { 109 | "node": ">=12" 110 | } 111 | }, 112 | "node_modules/@esbuild/freebsd-arm64": { 113 | "version": "0.19.6", 114 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.6.tgz", 115 | "integrity": "sha512-J53d0jGsDcLzWk9d9SPmlyF+wzVxjXpOH7jVW5ae7PvrDst4kiAz6sX+E8btz0GB6oH12zC+aHRD945jdjF2Vg==", 116 | "cpu": [ 117 | "arm64" 118 | ], 119 | "dev": true, 120 | "optional": true, 121 | "os": [ 122 | "freebsd" 123 | ], 124 | "engines": { 125 | "node": ">=12" 126 | } 127 | }, 128 | "node_modules/@esbuild/freebsd-x64": { 129 | "version": "0.19.6", 130 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.6.tgz", 131 | "integrity": "sha512-hn9qvkjHSIB5Z9JgCCjED6YYVGCNpqB7dEGavBdG6EjBD8S/UcNUIlGcB35NCkMETkdYwfZSvD9VoDJX6VeUVA==", 132 | "cpu": [ 133 | "x64" 134 | ], 135 | "dev": true, 136 | "optional": true, 137 | "os": [ 138 | "freebsd" 139 | ], 140 | "engines": { 141 | "node": ">=12" 142 | } 143 | }, 144 | "node_modules/@esbuild/linux-arm": { 145 | "version": "0.19.6", 146 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.6.tgz", 147 | "integrity": "sha512-G8IR5zFgpXad/Zp7gr7ZyTKyqZuThU6z1JjmRyN1vSF8j0bOlGzUwFSMTbctLAdd7QHpeyu0cRiuKrqK1ZTwvQ==", 148 | "cpu": [ 149 | "arm" 150 | ], 151 | "dev": true, 152 | "optional": true, 153 | "os": [ 154 | "linux" 155 | ], 156 | "engines": { 157 | "node": ">=12" 158 | } 159 | }, 160 | "node_modules/@esbuild/linux-arm64": { 161 | "version": "0.19.6", 162 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.6.tgz", 163 | "integrity": "sha512-HQCOrk9XlH3KngASLaBfHpcoYEGUt829A9MyxaI8RMkfRA8SakG6YQEITAuwmtzFdEu5GU4eyhKcpv27dFaOBg==", 164 | "cpu": [ 165 | "arm64" 166 | ], 167 | "dev": true, 168 | "optional": true, 169 | "os": [ 170 | "linux" 171 | ], 172 | "engines": { 173 | "node": ">=12" 174 | } 175 | }, 176 | "node_modules/@esbuild/linux-ia32": { 177 | "version": "0.19.6", 178 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.6.tgz", 179 | "integrity": "sha512-22eOR08zL/OXkmEhxOfshfOGo8P69k8oKHkwkDrUlcB12S/sw/+COM4PhAPT0cAYW/gpqY2uXp3TpjQVJitz7w==", 180 | "cpu": [ 181 | "ia32" 182 | ], 183 | "dev": true, 184 | "optional": true, 185 | "os": [ 186 | "linux" 187 | ], 188 | "engines": { 189 | "node": ">=12" 190 | } 191 | }, 192 | "node_modules/@esbuild/linux-loong64": { 193 | "version": "0.19.6", 194 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.6.tgz", 195 | "integrity": "sha512-82RvaYAh/SUJyjWA8jDpyZCHQjmEggL//sC7F3VKYcBMumQjUL3C5WDl/tJpEiKtt7XrWmgjaLkrk205zfvwTA==", 196 | "cpu": [ 197 | "loong64" 198 | ], 199 | "dev": true, 200 | "optional": true, 201 | "os": [ 202 | "linux" 203 | ], 204 | "engines": { 205 | "node": ">=12" 206 | } 207 | }, 208 | "node_modules/@esbuild/linux-mips64el": { 209 | "version": "0.19.6", 210 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.6.tgz", 211 | "integrity": "sha512-8tvnwyYJpR618vboIv2l8tK2SuK/RqUIGMfMENkeDGo3hsEIrpGldMGYFcWxWeEILe5Fi72zoXLmhZ7PR23oQA==", 212 | "cpu": [ 213 | "mips64el" 214 | ], 215 | "dev": true, 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ], 220 | "engines": { 221 | "node": ">=12" 222 | } 223 | }, 224 | "node_modules/@esbuild/linux-ppc64": { 225 | "version": "0.19.6", 226 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.6.tgz", 227 | "integrity": "sha512-Qt+D7xiPajxVNk5tQiEJwhmarNnLPdjXAoA5uWMpbfStZB0+YU6a3CtbWYSy+sgAsnyx4IGZjWsTzBzrvg/fMA==", 228 | "cpu": [ 229 | "ppc64" 230 | ], 231 | "dev": true, 232 | "optional": true, 233 | "os": [ 234 | "linux" 235 | ], 236 | "engines": { 237 | "node": ">=12" 238 | } 239 | }, 240 | "node_modules/@esbuild/linux-riscv64": { 241 | "version": "0.19.6", 242 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.6.tgz", 243 | "integrity": "sha512-lxRdk0iJ9CWYDH1Wpnnnc640ajF4RmQ+w6oHFZmAIYu577meE9Ka/DCtpOrwr9McMY11ocbp4jirgGgCi7Ls/g==", 244 | "cpu": [ 245 | "riscv64" 246 | ], 247 | "dev": true, 248 | "optional": true, 249 | "os": [ 250 | "linux" 251 | ], 252 | "engines": { 253 | "node": ">=12" 254 | } 255 | }, 256 | "node_modules/@esbuild/linux-s390x": { 257 | "version": "0.19.6", 258 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.6.tgz", 259 | "integrity": "sha512-MopyYV39vnfuykHanRWHGRcRC3AwU7b0QY4TI8ISLfAGfK+tMkXyFuyT1epw/lM0pflQlS53JoD22yN83DHZgA==", 260 | "cpu": [ 261 | "s390x" 262 | ], 263 | "dev": true, 264 | "optional": true, 265 | "os": [ 266 | "linux" 267 | ], 268 | "engines": { 269 | "node": ">=12" 270 | } 271 | }, 272 | "node_modules/@esbuild/linux-x64": { 273 | "version": "0.19.6", 274 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.6.tgz", 275 | "integrity": "sha512-UWcieaBzsN8WYbzFF5Jq7QULETPcQvlX7KL4xWGIB54OknXJjBO37sPqk7N82WU13JGWvmDzFBi1weVBajPovg==", 276 | "cpu": [ 277 | "x64" 278 | ], 279 | "dev": true, 280 | "optional": true, 281 | "os": [ 282 | "linux" 283 | ], 284 | "engines": { 285 | "node": ">=12" 286 | } 287 | }, 288 | "node_modules/@esbuild/netbsd-x64": { 289 | "version": "0.19.6", 290 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.6.tgz", 291 | "integrity": "sha512-EpWiLX0fzvZn1wxtLxZrEW+oQED9Pwpnh+w4Ffv8ZLuMhUoqR9q9rL4+qHW8F4Mg5oQEKxAoT0G+8JYNqCiR6g==", 292 | "cpu": [ 293 | "x64" 294 | ], 295 | "dev": true, 296 | "optional": true, 297 | "os": [ 298 | "netbsd" 299 | ], 300 | "engines": { 301 | "node": ">=12" 302 | } 303 | }, 304 | "node_modules/@esbuild/openbsd-x64": { 305 | "version": "0.19.6", 306 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.6.tgz", 307 | "integrity": "sha512-fFqTVEktM1PGs2sLKH4M5mhAVEzGpeZJuasAMRnvDZNCV0Cjvm1Hu35moL2vC0DOrAQjNTvj4zWrol/lwQ8Deg==", 308 | "cpu": [ 309 | "x64" 310 | ], 311 | "dev": true, 312 | "optional": true, 313 | "os": [ 314 | "openbsd" 315 | ], 316 | "engines": { 317 | "node": ">=12" 318 | } 319 | }, 320 | "node_modules/@esbuild/sunos-x64": { 321 | "version": "0.19.6", 322 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.6.tgz", 323 | "integrity": "sha512-M+XIAnBpaNvaVAhbe3uBXtgWyWynSdlww/JNZws0FlMPSBy+EpatPXNIlKAdtbFVII9OpX91ZfMb17TU3JKTBA==", 324 | "cpu": [ 325 | "x64" 326 | ], 327 | "dev": true, 328 | "optional": true, 329 | "os": [ 330 | "sunos" 331 | ], 332 | "engines": { 333 | "node": ">=12" 334 | } 335 | }, 336 | "node_modules/@esbuild/win32-arm64": { 337 | "version": "0.19.6", 338 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.6.tgz", 339 | "integrity": "sha512-2DchFXn7vp/B6Tc2eKdTsLzE0ygqKkNUhUBCNtMx2Llk4POIVMUq5rUYjdcedFlGLeRe1uLCpVvCmE+G8XYybA==", 340 | "cpu": [ 341 | "arm64" 342 | ], 343 | "dev": true, 344 | "optional": true, 345 | "os": [ 346 | "win32" 347 | ], 348 | "engines": { 349 | "node": ">=12" 350 | } 351 | }, 352 | "node_modules/@esbuild/win32-ia32": { 353 | "version": "0.19.6", 354 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.6.tgz", 355 | "integrity": "sha512-PBo/HPDQllyWdjwAVX+Gl2hH0dfBydL97BAH/grHKC8fubqp02aL4S63otZ25q3sBdINtOBbz1qTZQfXbP4VBg==", 356 | "cpu": [ 357 | "ia32" 358 | ], 359 | "dev": true, 360 | "optional": true, 361 | "os": [ 362 | "win32" 363 | ], 364 | "engines": { 365 | "node": ">=12" 366 | } 367 | }, 368 | "node_modules/@esbuild/win32-x64": { 369 | "version": "0.19.6", 370 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.6.tgz", 371 | "integrity": "sha512-OE7yIdbDif2kKfrGa+V0vx/B3FJv2L4KnIiLlvtibPyO9UkgO3rzYE0HhpREo2vmJ1Ixq1zwm9/0er+3VOSZJA==", 372 | "cpu": [ 373 | "x64" 374 | ], 375 | "dev": true, 376 | "optional": true, 377 | "os": [ 378 | "win32" 379 | ], 380 | "engines": { 381 | "node": ">=12" 382 | } 383 | }, 384 | "node_modules/@extism/js-pdk": { 385 | "resolved": "../../crates/core/src/prelude", 386 | "link": true 387 | }, 388 | "node_modules/cross-env": { 389 | "version": "7.0.3", 390 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 391 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 392 | "dev": true, 393 | "dependencies": { 394 | "cross-spawn": "^7.0.1" 395 | }, 396 | "bin": { 397 | "cross-env": "src/bin/cross-env.js", 398 | "cross-env-shell": "src/bin/cross-env-shell.js" 399 | }, 400 | "engines": { 401 | "node": ">=10.14", 402 | "npm": ">=6", 403 | "yarn": ">=1" 404 | } 405 | }, 406 | "node_modules/cross-spawn": { 407 | "version": "7.0.3", 408 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 409 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 410 | "dev": true, 411 | "dependencies": { 412 | "path-key": "^3.1.0", 413 | "shebang-command": "^2.0.0", 414 | "which": "^2.0.1" 415 | }, 416 | "engines": { 417 | "node": ">= 8" 418 | } 419 | }, 420 | "node_modules/esbuild": { 421 | "version": "0.19.6", 422 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.6.tgz", 423 | "integrity": "sha512-Xl7dntjA2OEIvpr9j0DVxxnog2fyTGnyVoQXAMQI6eR3mf9zCQds7VIKUDCotDgE/p4ncTgeRqgX8t5d6oP4Gw==", 424 | "dev": true, 425 | "hasInstallScript": true, 426 | "bin": { 427 | "esbuild": "bin/esbuild" 428 | }, 429 | "engines": { 430 | "node": ">=12" 431 | }, 432 | "optionalDependencies": { 433 | "@esbuild/android-arm": "0.19.6", 434 | "@esbuild/android-arm64": "0.19.6", 435 | "@esbuild/android-x64": "0.19.6", 436 | "@esbuild/darwin-arm64": "0.19.6", 437 | "@esbuild/darwin-x64": "0.19.6", 438 | "@esbuild/freebsd-arm64": "0.19.6", 439 | "@esbuild/freebsd-x64": "0.19.6", 440 | "@esbuild/linux-arm": "0.19.6", 441 | "@esbuild/linux-arm64": "0.19.6", 442 | "@esbuild/linux-ia32": "0.19.6", 443 | "@esbuild/linux-loong64": "0.19.6", 444 | "@esbuild/linux-mips64el": "0.19.6", 445 | "@esbuild/linux-ppc64": "0.19.6", 446 | "@esbuild/linux-riscv64": "0.19.6", 447 | "@esbuild/linux-s390x": "0.19.6", 448 | "@esbuild/linux-x64": "0.19.6", 449 | "@esbuild/netbsd-x64": "0.19.6", 450 | "@esbuild/openbsd-x64": "0.19.6", 451 | "@esbuild/sunos-x64": "0.19.6", 452 | "@esbuild/win32-arm64": "0.19.6", 453 | "@esbuild/win32-ia32": "0.19.6", 454 | "@esbuild/win32-x64": "0.19.6" 455 | } 456 | }, 457 | "node_modules/isexe": { 458 | "version": "2.0.0", 459 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 460 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 461 | "dev": true 462 | }, 463 | "node_modules/path-key": { 464 | "version": "3.1.1", 465 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 466 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 467 | "dev": true, 468 | "engines": { 469 | "node": ">=8" 470 | } 471 | }, 472 | "node_modules/shebang-command": { 473 | "version": "2.0.0", 474 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 475 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 476 | "dev": true, 477 | "dependencies": { 478 | "shebang-regex": "^3.0.0" 479 | }, 480 | "engines": { 481 | "node": ">=8" 482 | } 483 | }, 484 | "node_modules/shebang-regex": { 485 | "version": "3.0.0", 486 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 487 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 488 | "dev": true, 489 | "engines": { 490 | "node": ">=8" 491 | } 492 | }, 493 | "node_modules/typescript": { 494 | "version": "5.3.2", 495 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", 496 | "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", 497 | "dev": true, 498 | "bin": { 499 | "tsc": "bin/tsc", 500 | "tsserver": "bin/tsserver" 501 | }, 502 | "engines": { 503 | "node": ">=14.17" 504 | } 505 | }, 506 | "node_modules/which": { 507 | "version": "2.0.2", 508 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 509 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 510 | "dev": true, 511 | "dependencies": { 512 | "isexe": "^2.0.0" 513 | }, 514 | "bin": { 515 | "node-which": "bin/node-which" 516 | }, 517 | "engines": { 518 | "node": ">= 8" 519 | } 520 | } 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /examples/bundled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundled-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production node esbuild.js && cross-env ../../target/release/extism-js dist/index.js -i src/index.d.ts -o ../bundled.wasm" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "BSD-3-Clause", 12 | "devDependencies": { 13 | "@extism/js-pdk": "../../crates/core/src/prelude", 14 | "cross-env": "^7.0.3", 15 | "esbuild": "^0.19.6", 16 | "typescript": "^5.3.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/bundled/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/bundled/src/index.ts: -------------------------------------------------------------------------------- 1 | export function greet() { 2 | Var.set("name", "MAYBESteve"); 3 | let extra = new TextEncoder().encode("aaa"); 4 | let decoded = new TextDecoder().decode(extra); 5 | const res = Http.request({ url: "http://example.com", method: "GET" }); 6 | const name = Var.getString("name") || "unknown"; 7 | const apiKey = Config.get("SOME_API_KEY") || "unknown"; 8 | 9 | Host.outputString( 10 | `Hello, ${Host.inputString()} (or is it ${name}???) ${decoded} ${new Date().toString()}\n\n${ 11 | res.body 12 | }\n\n ==== KEY: ${apiKey}` 13 | ); 14 | } 15 | 16 | // test this bundled.wasm like so: 17 | // extism call ../bundled.wasm greet --input "steve" --wasi --allow-host "*" --config SOME_API_KEY=123456789 18 | -------------------------------------------------------------------------------- /examples/bundled/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["@extism/js-pdk"], 5 | "noEmit": true 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/console/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["../../crates/core/src/prelude"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/console/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/console/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An example of a plugin that uses Console extensively, CJS flavored plug-in: 3 | */ 4 | 5 | function greet() { 6 | const n = 1; 7 | tryPrint('n + 1', n + 1); 8 | tryPrint('multiple string args', 'one', 'two', 'three'); 9 | tryPrint('single n', n); 10 | tryPrint('three ns', n, n, n); 11 | tryPrint('n with label', 'n', n); 12 | tryPrint('boolean', true); 13 | tryPrint('null', null); 14 | tryPrint('undefined', undefined); 15 | tryPrint('empty object', {}); 16 | tryPrint('empty array', []); 17 | tryPrint('object with key', { key: 'value' }); 18 | console.warn('This is a warning', 123); 19 | console.error('This is an error', 456); 20 | console.info('This is an info', 789); 21 | console.debug('This is a debug', 101112); 22 | console.trace('This is a trace', 131415); 23 | 24 | console.log('This is an object', { key: 'value' }); 25 | console.log('This is an array', [1, 2, 3]); 26 | console.log('This is a string', 'Hello, World!'); 27 | console.log('This is a number', 123); 28 | console.log('This is a boolean', true); 29 | console.log('This is a null', null); 30 | console.log('This is an undefined', undefined); 31 | console.log('This is a function', function() {}); 32 | console.log('This is a symbol', Symbol('test')); 33 | console.log('This is a date', new Date()); 34 | console.log('This is an error', new Error('Hi there!')); 35 | console.log('This is a map', new Map([[1, 'one'], [2, 'two']] )); 36 | } 37 | 38 | function tryPrint(text, ...args) { 39 | try { 40 | console.log(...args); 41 | console.log(`${text} - ✅`); 42 | } catch (e) { 43 | console.log(`${text} - ❌ - ${e.message}`); 44 | } 45 | console.log('------') 46 | } 47 | 48 | 49 | module.exports = { greet }; 50 | -------------------------------------------------------------------------------- /examples/exception/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": [ 5 | "../../crates/core/src/prelude" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/exception/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/exception/script.js: -------------------------------------------------------------------------------- 1 | 2 | function greet() { 3 | throw new Error("I am a JS exception shibboleth") 4 | } 5 | 6 | module.exports = { greet }; 7 | -------------------------------------------------------------------------------- /examples/exports/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["../../crates/core/src/prelude"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/exports/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'main' { 2 | export function add3(a: I32, b: I32, c: I32): I32; 3 | export function appendString(a: PTR, b: PTR): PTR; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /examples/exports/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple example of generate non-plugin function exports 3 | */ 4 | 5 | function add3(a, b, c) { 6 | return a + b + c; 7 | } 8 | 9 | function appendString(a, b) { 10 | a = Memory.find(a).readString(); 11 | b = Memory.find(b).readString(); 12 | return Memory.fromString(a + b).offset; 13 | } 14 | 15 | module.exports = { add3, appendString }; 16 | -------------------------------------------------------------------------------- /examples/host_funcs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/extism/js-pdk/examples/host_funcs 2 | 3 | go 1.23.4 4 | 5 | require github.com/extism/go-sdk v1.6.1 6 | 7 | require ( 8 | github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect 9 | github.com/gobwas/glob v0.2.3 // indirect 10 | github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect 11 | github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect 12 | github.com/tetratelabs/wazero v1.8.1-0.20240916092830-1353ca24fef0 // indirect 13 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 14 | google.golang.org/protobuf v1.34.2 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /examples/host_funcs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= 4 | github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= 5 | github.com/extism/go-sdk v1.6.1 h1:gkbkG5KzYKrv8mLggw5ojg/JulXfEbLIRVhbw9Ot7S0= 6 | github.com/extism/go-sdk v1.6.1/go.mod h1:yRolc4PvIUQ9J/BBB3QZ5EY1MtXAN2jqBGDGR3Sk54M= 7 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 8 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 9 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 10 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 11 | github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= 12 | github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 16 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= 18 | github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= 19 | github.com/tetratelabs/wazero v1.8.1-0.20240916092830-1353ca24fef0 h1:NCRnJ+X6eZt3awiReoHCcDuC6Wf+CgWk6p4IDkIuxTo= 20 | github.com/tetratelabs/wazero v1.8.1-0.20240916092830-1353ca24fef0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 21 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 22 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 24 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 26 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /examples/host_funcs/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["../../crates/core/src/prelude"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/host_funcs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "os" 8 | "strings" 9 | 10 | extism "github.com/extism/go-sdk" 11 | ) 12 | 13 | // Capitalize a string (Host function for `capitalize`) 14 | func capitalize(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { 15 | input, err := p.ReadString(stack[0]) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | capitalized := strings.Title(input) 21 | s, err := p.WriteString(capitalized) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | stack[0] = s 27 | } 28 | 29 | // Handle floating point inputs (Host function for `floatInputs`) 30 | func floatInputs(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { 31 | f64 := extism.DecodeF64(stack[0]) 32 | f32 := float32(extism.DecodeF32(stack[1])) 33 | 34 | fmt.Println("Go Host: floatInputs received:", f64, f32) 35 | 36 | // Return a fixed integer (expected behavior based on JavaScript code) 37 | stack[0] = uint64(2_147_483_647) // Max int32 value 38 | } 39 | 40 | // Handle integer input, return a floating point (Host function for `floatOutput`) 41 | func floatOutput(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { 42 | i32 := int32(stack[0]) 43 | 44 | fmt.Println("Go Host: floatOutput received:", i32) 45 | 46 | // Return the expected float value 47 | result := 9_007_199_254_740.125 48 | stack[0] = extism.EncodeF64(result) 49 | } 50 | 51 | // Handle multiple arguments but return nothing (Host function for `voidInputs`) 52 | func voidInputs(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { 53 | fmt.Printf("Go Host: voidInputs stack: %v, %v, %v, %v, %v\n", stack[0], stack[1], stack[2], stack[3], stack[4]) 54 | 55 | i32 := int32(stack[0]) 56 | i64 := int64(stack[1]) 57 | f32 := float32(extism.DecodeF32(stack[2])) 58 | f64 := extism.DecodeF64(stack[3]) 59 | extra := int32(stack[4]) 60 | 61 | fmt.Printf("Go Host: voidInputs received: i32=%d, i64=%d, f32=%f, f64=%f, extra=%d\n", i32, i64, f32, f64, extra) 62 | 63 | if i32 != 2_147_483_647 { 64 | panic("Unexpected i32 value: " + fmt.Sprint(i32)) 65 | } 66 | 67 | if i64 != 9_223_372_036_854_775_807 { 68 | panic("Unexpected i64 value: " + fmt.Sprint(i64)) 69 | } 70 | 71 | if math.Abs(float64(f32-314_567.5)) > 0.0001 { 72 | panic("Unexpected f32 value: " + fmt.Sprint(f32)) 73 | } 74 | 75 | if math.Abs(f64-9_007_199_254_740.125) > 0.0001 { 76 | panic("Unexpected f64 value: " + fmt.Sprint(f64)) 77 | } 78 | } 79 | 80 | func main() { 81 | if len(os.Args) < 2 { 82 | fmt.Println("Usage: go run main.go ") 83 | os.Exit(1) 84 | } 85 | 86 | wasmFile := os.Args[1] 87 | data, err := os.ReadFile(wasmFile) 88 | if err != nil { 89 | fmt.Printf("Failed to read wasm file: %v\n", err) 90 | os.Exit(1) 91 | } 92 | 93 | manifest := extism.Manifest{ 94 | Wasm: []extism.Wasm{extism.WasmData{Data: data}}, 95 | } 96 | 97 | extism.SetLogLevel(extism.LogLevelDebug) 98 | 99 | ctx := context.Background() 100 | config := extism.PluginConfig{EnableWasi: true} 101 | plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{ 102 | extism.NewHostFunctionWithStack("capitalize", capitalize, []extism.ValueType{extism.ValueTypePTR}, []extism.ValueType{extism.ValueTypePTR}), 103 | extism.NewHostFunctionWithStack("floatInputs", floatInputs, []extism.ValueType{extism.ValueTypeF64, extism.ValueTypeF32}, []extism.ValueType{extism.ValueTypeI32}), 104 | extism.NewHostFunctionWithStack("floatOutput", floatOutput, []extism.ValueType{extism.ValueTypeI32}, []extism.ValueType{extism.ValueTypeF64}), 105 | extism.NewHostFunctionWithStack("voidInputs", voidInputs, []extism.ValueType{extism.ValueTypeI32, extism.ValueTypeI64, extism.ValueTypeF32, extism.ValueTypeF64, extism.ValueTypeI32}, []extism.ValueType{}), 106 | }) 107 | 108 | if err != nil { 109 | fmt.Printf("Failed to initialize plugin: %v\n", err) 110 | os.Exit(1) 111 | } 112 | 113 | exit, result, err := plugin.Call("greet", []byte("Benjamin")) 114 | if err != nil { 115 | fmt.Printf("Plugin call failed: %v\n", err) 116 | os.Exit(int(exit)) 117 | } 118 | 119 | fmt.Println(string(result)) 120 | } 121 | -------------------------------------------------------------------------------- /examples/host_funcs/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'main' { 2 | export function greet(): I32; 3 | } 4 | 5 | declare module 'extism:host' { 6 | interface user { 7 | capitalize(ptr: PTR): PTR; 8 | floatInputs(p1: F64, p2: F32): I32; 9 | floatOutput(p1: I32): F64; 10 | voidInputs(p1: I32, p2: I64, p3: F32, p4: F64, p5: I32): void; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/host_funcs/script.js: -------------------------------------------------------------------------------- 1 | // Extract host functions by name. 2 | // Note: these must be declared in the d.ts file 3 | const { capitalize, floatInputs, floatOutput, voidInputs } = Host.getFunctions() 4 | 5 | function greet() { 6 | const name = Host.inputString(); 7 | const mem = Memory.fromString(name); 8 | const capNameOffset = capitalize(mem.offset); 9 | const capName = mem.readString(capNameOffset); 10 | console.log(`Hello, ${capName}!`); 11 | 12 | const f32 = 314_567.5; 13 | const i32 = 2_147_483_647; 14 | const f64 = 9_007_199_254_740.125; 15 | const i64 = 9_223_372_036_854_775_807; 16 | 17 | const num1 = floatInputs(f64, f32); 18 | console.log(`floatInputs result: ${num1}`); 19 | if (num1 != i32) { 20 | throw new Error(`Unexpected floatInputs result: ${num1}. Expected: ${i32}`); 21 | } 22 | 23 | const num2 = floatOutput(i32); 24 | console.log(`floatOutput result: ${num2}`); 25 | if (Math.abs(num2 - f64) >= 0.001) { 26 | throw new Error(`Unexpected floatOutput result: ${num2}. Expected: ${f64}`); 27 | } 28 | 29 | voidInputs(i32, i64, f32, f64, i32); 30 | 31 | console.log("All tests passed!"); 32 | Host.outputString(`Hello, ${capName}!`); 33 | } 34 | 35 | module.exports = { greet } 36 | -------------------------------------------------------------------------------- /examples/kitchen-sink/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /examples/kitchen-sink/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024, The Extism Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /examples/kitchen-sink/README.md: -------------------------------------------------------------------------------- 1 | # Extism JS PDK Plugin 2 | 3 | See more documentation at https://github.com/extism/js-pdk and 4 | [join us on Discord](https://extism.org/discord) for more help. 5 | -------------------------------------------------------------------------------- /examples/kitchen-sink/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | esbuild 4 | .build({ 5 | entryPoints: ['src/index.ts'], 6 | outdir: 'dist', 7 | bundle: true, 8 | sourcemap: true, 9 | minify: false, // might want to use true for production build 10 | format: 'cjs', // needs to be CJS for now 11 | target: ['es2020'] // don't go over es2020 because quickjs doesn't support it 12 | }) -------------------------------------------------------------------------------- /examples/kitchen-sink/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kitchen-sink", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "kitchen-sink", 9 | "version": "1.0.0", 10 | "license": "BSD-3-Clause", 11 | "devDependencies": { 12 | "@extism/js-pdk": "../../crates/core/src/prelude", 13 | "cross-env": "^7.0.3", 14 | "esbuild": "^0.19.6", 15 | "typescript": "^5.3.2" 16 | } 17 | }, 18 | "../../crates/core/src/prelude": { 19 | "name": "@extism/js-pdk", 20 | "version": "1.1.1", 21 | "dev": true, 22 | "license": "BSD-Clause-3", 23 | "dependencies": { 24 | "urlpattern-polyfill": "^8.0.2" 25 | }, 26 | "devDependencies": { 27 | "core-js": "^3.30.2", 28 | "esbuild": "^0.17.19", 29 | "typescript": "^5.4.5" 30 | } 31 | }, 32 | "node_modules/@esbuild/aix-ppc64": { 33 | "version": "0.19.12", 34 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", 35 | "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", 36 | "cpu": [ 37 | "ppc64" 38 | ], 39 | "dev": true, 40 | "optional": true, 41 | "os": [ 42 | "aix" 43 | ], 44 | "engines": { 45 | "node": ">=12" 46 | } 47 | }, 48 | "node_modules/@esbuild/android-arm": { 49 | "version": "0.19.12", 50 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", 51 | "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", 52 | "cpu": [ 53 | "arm" 54 | ], 55 | "dev": true, 56 | "optional": true, 57 | "os": [ 58 | "android" 59 | ], 60 | "engines": { 61 | "node": ">=12" 62 | } 63 | }, 64 | "node_modules/@esbuild/android-arm64": { 65 | "version": "0.19.12", 66 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", 67 | "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", 68 | "cpu": [ 69 | "arm64" 70 | ], 71 | "dev": true, 72 | "optional": true, 73 | "os": [ 74 | "android" 75 | ], 76 | "engines": { 77 | "node": ">=12" 78 | } 79 | }, 80 | "node_modules/@esbuild/android-x64": { 81 | "version": "0.19.12", 82 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", 83 | "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", 84 | "cpu": [ 85 | "x64" 86 | ], 87 | "dev": true, 88 | "optional": true, 89 | "os": [ 90 | "android" 91 | ], 92 | "engines": { 93 | "node": ">=12" 94 | } 95 | }, 96 | "node_modules/@esbuild/darwin-arm64": { 97 | "version": "0.19.12", 98 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", 99 | "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", 100 | "cpu": [ 101 | "arm64" 102 | ], 103 | "dev": true, 104 | "optional": true, 105 | "os": [ 106 | "darwin" 107 | ], 108 | "engines": { 109 | "node": ">=12" 110 | } 111 | }, 112 | "node_modules/@esbuild/darwin-x64": { 113 | "version": "0.19.12", 114 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", 115 | "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", 116 | "cpu": [ 117 | "x64" 118 | ], 119 | "dev": true, 120 | "optional": true, 121 | "os": [ 122 | "darwin" 123 | ], 124 | "engines": { 125 | "node": ">=12" 126 | } 127 | }, 128 | "node_modules/@esbuild/freebsd-arm64": { 129 | "version": "0.19.12", 130 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", 131 | "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", 132 | "cpu": [ 133 | "arm64" 134 | ], 135 | "dev": true, 136 | "optional": true, 137 | "os": [ 138 | "freebsd" 139 | ], 140 | "engines": { 141 | "node": ">=12" 142 | } 143 | }, 144 | "node_modules/@esbuild/freebsd-x64": { 145 | "version": "0.19.12", 146 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", 147 | "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", 148 | "cpu": [ 149 | "x64" 150 | ], 151 | "dev": true, 152 | "optional": true, 153 | "os": [ 154 | "freebsd" 155 | ], 156 | "engines": { 157 | "node": ">=12" 158 | } 159 | }, 160 | "node_modules/@esbuild/linux-arm": { 161 | "version": "0.19.12", 162 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", 163 | "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", 164 | "cpu": [ 165 | "arm" 166 | ], 167 | "dev": true, 168 | "optional": true, 169 | "os": [ 170 | "linux" 171 | ], 172 | "engines": { 173 | "node": ">=12" 174 | } 175 | }, 176 | "node_modules/@esbuild/linux-arm64": { 177 | "version": "0.19.12", 178 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", 179 | "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", 180 | "cpu": [ 181 | "arm64" 182 | ], 183 | "dev": true, 184 | "optional": true, 185 | "os": [ 186 | "linux" 187 | ], 188 | "engines": { 189 | "node": ">=12" 190 | } 191 | }, 192 | "node_modules/@esbuild/linux-ia32": { 193 | "version": "0.19.12", 194 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", 195 | "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", 196 | "cpu": [ 197 | "ia32" 198 | ], 199 | "dev": true, 200 | "optional": true, 201 | "os": [ 202 | "linux" 203 | ], 204 | "engines": { 205 | "node": ">=12" 206 | } 207 | }, 208 | "node_modules/@esbuild/linux-loong64": { 209 | "version": "0.19.12", 210 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", 211 | "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", 212 | "cpu": [ 213 | "loong64" 214 | ], 215 | "dev": true, 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ], 220 | "engines": { 221 | "node": ">=12" 222 | } 223 | }, 224 | "node_modules/@esbuild/linux-mips64el": { 225 | "version": "0.19.12", 226 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", 227 | "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", 228 | "cpu": [ 229 | "mips64el" 230 | ], 231 | "dev": true, 232 | "optional": true, 233 | "os": [ 234 | "linux" 235 | ], 236 | "engines": { 237 | "node": ">=12" 238 | } 239 | }, 240 | "node_modules/@esbuild/linux-ppc64": { 241 | "version": "0.19.12", 242 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", 243 | "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", 244 | "cpu": [ 245 | "ppc64" 246 | ], 247 | "dev": true, 248 | "optional": true, 249 | "os": [ 250 | "linux" 251 | ], 252 | "engines": { 253 | "node": ">=12" 254 | } 255 | }, 256 | "node_modules/@esbuild/linux-riscv64": { 257 | "version": "0.19.12", 258 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", 259 | "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", 260 | "cpu": [ 261 | "riscv64" 262 | ], 263 | "dev": true, 264 | "optional": true, 265 | "os": [ 266 | "linux" 267 | ], 268 | "engines": { 269 | "node": ">=12" 270 | } 271 | }, 272 | "node_modules/@esbuild/linux-s390x": { 273 | "version": "0.19.12", 274 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", 275 | "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", 276 | "cpu": [ 277 | "s390x" 278 | ], 279 | "dev": true, 280 | "optional": true, 281 | "os": [ 282 | "linux" 283 | ], 284 | "engines": { 285 | "node": ">=12" 286 | } 287 | }, 288 | "node_modules/@esbuild/linux-x64": { 289 | "version": "0.19.12", 290 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", 291 | "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", 292 | "cpu": [ 293 | "x64" 294 | ], 295 | "dev": true, 296 | "optional": true, 297 | "os": [ 298 | "linux" 299 | ], 300 | "engines": { 301 | "node": ">=12" 302 | } 303 | }, 304 | "node_modules/@esbuild/netbsd-x64": { 305 | "version": "0.19.12", 306 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", 307 | "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", 308 | "cpu": [ 309 | "x64" 310 | ], 311 | "dev": true, 312 | "optional": true, 313 | "os": [ 314 | "netbsd" 315 | ], 316 | "engines": { 317 | "node": ">=12" 318 | } 319 | }, 320 | "node_modules/@esbuild/openbsd-x64": { 321 | "version": "0.19.12", 322 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", 323 | "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", 324 | "cpu": [ 325 | "x64" 326 | ], 327 | "dev": true, 328 | "optional": true, 329 | "os": [ 330 | "openbsd" 331 | ], 332 | "engines": { 333 | "node": ">=12" 334 | } 335 | }, 336 | "node_modules/@esbuild/sunos-x64": { 337 | "version": "0.19.12", 338 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", 339 | "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", 340 | "cpu": [ 341 | "x64" 342 | ], 343 | "dev": true, 344 | "optional": true, 345 | "os": [ 346 | "sunos" 347 | ], 348 | "engines": { 349 | "node": ">=12" 350 | } 351 | }, 352 | "node_modules/@esbuild/win32-arm64": { 353 | "version": "0.19.12", 354 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", 355 | "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", 356 | "cpu": [ 357 | "arm64" 358 | ], 359 | "dev": true, 360 | "optional": true, 361 | "os": [ 362 | "win32" 363 | ], 364 | "engines": { 365 | "node": ">=12" 366 | } 367 | }, 368 | "node_modules/@esbuild/win32-ia32": { 369 | "version": "0.19.12", 370 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", 371 | "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", 372 | "cpu": [ 373 | "ia32" 374 | ], 375 | "dev": true, 376 | "optional": true, 377 | "os": [ 378 | "win32" 379 | ], 380 | "engines": { 381 | "node": ">=12" 382 | } 383 | }, 384 | "node_modules/@esbuild/win32-x64": { 385 | "version": "0.19.12", 386 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", 387 | "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", 388 | "cpu": [ 389 | "x64" 390 | ], 391 | "dev": true, 392 | "optional": true, 393 | "os": [ 394 | "win32" 395 | ], 396 | "engines": { 397 | "node": ">=12" 398 | } 399 | }, 400 | "node_modules/@extism/js-pdk": { 401 | "resolved": "../../crates/core/src/prelude", 402 | "link": true 403 | }, 404 | "node_modules/cross-env": { 405 | "version": "7.0.3", 406 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 407 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 408 | "dev": true, 409 | "dependencies": { 410 | "cross-spawn": "^7.0.1" 411 | }, 412 | "bin": { 413 | "cross-env": "src/bin/cross-env.js", 414 | "cross-env-shell": "src/bin/cross-env-shell.js" 415 | }, 416 | "engines": { 417 | "node": ">=10.14", 418 | "npm": ">=6", 419 | "yarn": ">=1" 420 | } 421 | }, 422 | "node_modules/cross-spawn": { 423 | "version": "7.0.3", 424 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 425 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 426 | "dev": true, 427 | "dependencies": { 428 | "path-key": "^3.1.0", 429 | "shebang-command": "^2.0.0", 430 | "which": "^2.0.1" 431 | }, 432 | "engines": { 433 | "node": ">= 8" 434 | } 435 | }, 436 | "node_modules/esbuild": { 437 | "version": "0.19.12", 438 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", 439 | "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", 440 | "dev": true, 441 | "hasInstallScript": true, 442 | "bin": { 443 | "esbuild": "bin/esbuild" 444 | }, 445 | "engines": { 446 | "node": ">=12" 447 | }, 448 | "optionalDependencies": { 449 | "@esbuild/aix-ppc64": "0.19.12", 450 | "@esbuild/android-arm": "0.19.12", 451 | "@esbuild/android-arm64": "0.19.12", 452 | "@esbuild/android-x64": "0.19.12", 453 | "@esbuild/darwin-arm64": "0.19.12", 454 | "@esbuild/darwin-x64": "0.19.12", 455 | "@esbuild/freebsd-arm64": "0.19.12", 456 | "@esbuild/freebsd-x64": "0.19.12", 457 | "@esbuild/linux-arm": "0.19.12", 458 | "@esbuild/linux-arm64": "0.19.12", 459 | "@esbuild/linux-ia32": "0.19.12", 460 | "@esbuild/linux-loong64": "0.19.12", 461 | "@esbuild/linux-mips64el": "0.19.12", 462 | "@esbuild/linux-ppc64": "0.19.12", 463 | "@esbuild/linux-riscv64": "0.19.12", 464 | "@esbuild/linux-s390x": "0.19.12", 465 | "@esbuild/linux-x64": "0.19.12", 466 | "@esbuild/netbsd-x64": "0.19.12", 467 | "@esbuild/openbsd-x64": "0.19.12", 468 | "@esbuild/sunos-x64": "0.19.12", 469 | "@esbuild/win32-arm64": "0.19.12", 470 | "@esbuild/win32-ia32": "0.19.12", 471 | "@esbuild/win32-x64": "0.19.12" 472 | } 473 | }, 474 | "node_modules/isexe": { 475 | "version": "2.0.0", 476 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 477 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 478 | "dev": true 479 | }, 480 | "node_modules/path-key": { 481 | "version": "3.1.1", 482 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 483 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 484 | "dev": true, 485 | "engines": { 486 | "node": ">=8" 487 | } 488 | }, 489 | "node_modules/shebang-command": { 490 | "version": "2.0.0", 491 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 492 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 493 | "dev": true, 494 | "dependencies": { 495 | "shebang-regex": "^3.0.0" 496 | }, 497 | "engines": { 498 | "node": ">=8" 499 | } 500 | }, 501 | "node_modules/shebang-regex": { 502 | "version": "3.0.0", 503 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 504 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 505 | "dev": true, 506 | "engines": { 507 | "node": ">=8" 508 | } 509 | }, 510 | "node_modules/typescript": { 511 | "version": "5.4.3", 512 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", 513 | "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", 514 | "dev": true, 515 | "bin": { 516 | "tsc": "bin/tsc", 517 | "tsserver": "bin/tsserver" 518 | }, 519 | "engines": { 520 | "node": ">=14.17" 521 | } 522 | }, 523 | "node_modules/which": { 524 | "version": "2.0.2", 525 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 526 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 527 | "dev": true, 528 | "dependencies": { 529 | "isexe": "^2.0.0" 530 | }, 531 | "bin": { 532 | "node-which": "bin/node-which" 533 | }, 534 | "engines": { 535 | "node": ">= 8" 536 | } 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /examples/kitchen-sink/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kitchen-sink", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production node esbuild.js && cross-env ../../target/release/extism-js dist/index.js -i src/index.d.ts -o ../kitchen-sink.wasm" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "BSD-3-Clause", 12 | "devDependencies": { 13 | "@extism/js-pdk": "../../crates/core/src/prelude", 14 | "cross-env": "^7.0.3", 15 | "esbuild": "^0.19.6", 16 | "typescript": "^5.3.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/kitchen-sink/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/kitchen-sink/src/index.ts: -------------------------------------------------------------------------------- 1 | export function greet() { 2 | let input = Host.inputString(); 3 | Var.set("input", input); 4 | const req: HttpRequest = { 5 | url: "https://postman-echo.com/post", 6 | method: "POST", 7 | }; 8 | let varInput = Var.getString("input"); 9 | if (!varInput) { 10 | Host.outputString("failed to get var: input"); 11 | return -1; 12 | } 13 | let resp = Http.request(req, varInput); 14 | if (resp.status !== 200) { 15 | return -2; 16 | } 17 | const body = JSON.parse(resp.body); 18 | if (body.data !== input) { 19 | Host.outputString("got unexpected output: " + body.data); 20 | return -3; 21 | } 22 | 23 | const configLastName = Config.get("last_name"); 24 | if (!configLastName) { 25 | Host.outputString("failed to get config: last_name"); 26 | return -4; 27 | } 28 | 29 | if (`${body.data} ${configLastName}` !== "Steve Manuel") { 30 | Host.outputString(`got unexpected output: ${body.data} ${configLastName}`); 31 | return -5; 32 | } 33 | 34 | const mem = Memory.fromString("Hello, " + body.data + " " + configLastName); 35 | Host.outputString(mem.readString()); // TODO: ideally have a way to output memory directly 36 | mem.free(); 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /examples/kitchen-sink/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["@extism/js-pdk"], 5 | "noEmit": true 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /examples/react/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | esbuild 4 | .build({ 5 | entryPoints: ['src/index.tsx'], 6 | outdir: 'dist', 7 | bundle: true, 8 | sourcemap: true, 9 | minify: false, // might want to use true for production build 10 | format: 'cjs', // needs to be CJS for now 11 | target: ['es2020'] // don't go over es2020 because quickjs doesn't support it 12 | }) 13 | -------------------------------------------------------------------------------- /examples/react/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-plugin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "react-plugin", 9 | "version": "1.0.0", 10 | "license": "BSD-3-Clause", 11 | "dependencies": { 12 | "react-dom": "^18.3.1" 13 | }, 14 | "devDependencies": { 15 | "@extism/js-pdk": "../../crates/core/src/prelude", 16 | "@types/react": "^18.3.1", 17 | "cross-env": "^7.0.3", 18 | "esbuild": "^0.19.6", 19 | "typescript": "^5.3.2" 20 | } 21 | }, 22 | "../../crates/core/src/prelude": { 23 | "name": "@extism/js-pdk", 24 | "version": "1.1.1", 25 | "dev": true, 26 | "license": "BSD-Clause-3", 27 | "dependencies": { 28 | "urlpattern-polyfill": "^8.0.2" 29 | }, 30 | "devDependencies": { 31 | "core-js": "^3.30.2", 32 | "esbuild": "^0.17.19", 33 | "typescript": "^5.4.5" 34 | } 35 | }, 36 | "node_modules/@esbuild/android-arm": { 37 | "version": "0.19.6", 38 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.6.tgz", 39 | "integrity": "sha512-muPzBqXJKCbMYoNbb1JpZh/ynl0xS6/+pLjrofcR3Nad82SbsCogYzUE6Aq9QT3cLP0jR/IVK/NHC9b90mSHtg==", 40 | "cpu": [ 41 | "arm" 42 | ], 43 | "dev": true, 44 | "optional": true, 45 | "os": [ 46 | "android" 47 | ], 48 | "engines": { 49 | "node": ">=12" 50 | } 51 | }, 52 | "node_modules/@esbuild/android-arm64": { 53 | "version": "0.19.6", 54 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.6.tgz", 55 | "integrity": "sha512-KQ/hbe9SJvIJ4sR+2PcZ41IBV+LPJyYp6V1K1P1xcMRup9iYsBoQn4MzE3mhMLOld27Au2eDcLlIREeKGUXpHQ==", 56 | "cpu": [ 57 | "arm64" 58 | ], 59 | "dev": true, 60 | "optional": true, 61 | "os": [ 62 | "android" 63 | ], 64 | "engines": { 65 | "node": ">=12" 66 | } 67 | }, 68 | "node_modules/@esbuild/android-x64": { 69 | "version": "0.19.6", 70 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.6.tgz", 71 | "integrity": "sha512-VVJVZQ7p5BBOKoNxd0Ly3xUM78Y4DyOoFKdkdAe2m11jbh0LEU4bPles4e/72EMl4tapko8o915UalN/5zhspg==", 72 | "cpu": [ 73 | "x64" 74 | ], 75 | "dev": true, 76 | "optional": true, 77 | "os": [ 78 | "android" 79 | ], 80 | "engines": { 81 | "node": ">=12" 82 | } 83 | }, 84 | "node_modules/@esbuild/darwin-arm64": { 85 | "version": "0.19.6", 86 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.6.tgz", 87 | "integrity": "sha512-91LoRp/uZAKx6ESNspL3I46ypwzdqyDLXZH7x2QYCLgtnaU08+AXEbabY2yExIz03/am0DivsTtbdxzGejfXpA==", 88 | "cpu": [ 89 | "arm64" 90 | ], 91 | "dev": true, 92 | "optional": true, 93 | "os": [ 94 | "darwin" 95 | ], 96 | "engines": { 97 | "node": ">=12" 98 | } 99 | }, 100 | "node_modules/@esbuild/darwin-x64": { 101 | "version": "0.19.6", 102 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.6.tgz", 103 | "integrity": "sha512-QCGHw770ubjBU1J3ZkFJh671MFajGTYMZumPs9E/rqU52md6lIil97BR0CbPq6U+vTh3xnTNDHKRdR8ggHnmxQ==", 104 | "cpu": [ 105 | "x64" 106 | ], 107 | "dev": true, 108 | "optional": true, 109 | "os": [ 110 | "darwin" 111 | ], 112 | "engines": { 113 | "node": ">=12" 114 | } 115 | }, 116 | "node_modules/@esbuild/freebsd-arm64": { 117 | "version": "0.19.6", 118 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.6.tgz", 119 | "integrity": "sha512-J53d0jGsDcLzWk9d9SPmlyF+wzVxjXpOH7jVW5ae7PvrDst4kiAz6sX+E8btz0GB6oH12zC+aHRD945jdjF2Vg==", 120 | "cpu": [ 121 | "arm64" 122 | ], 123 | "dev": true, 124 | "optional": true, 125 | "os": [ 126 | "freebsd" 127 | ], 128 | "engines": { 129 | "node": ">=12" 130 | } 131 | }, 132 | "node_modules/@esbuild/freebsd-x64": { 133 | "version": "0.19.6", 134 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.6.tgz", 135 | "integrity": "sha512-hn9qvkjHSIB5Z9JgCCjED6YYVGCNpqB7dEGavBdG6EjBD8S/UcNUIlGcB35NCkMETkdYwfZSvD9VoDJX6VeUVA==", 136 | "cpu": [ 137 | "x64" 138 | ], 139 | "dev": true, 140 | "optional": true, 141 | "os": [ 142 | "freebsd" 143 | ], 144 | "engines": { 145 | "node": ">=12" 146 | } 147 | }, 148 | "node_modules/@esbuild/linux-arm": { 149 | "version": "0.19.6", 150 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.6.tgz", 151 | "integrity": "sha512-G8IR5zFgpXad/Zp7gr7ZyTKyqZuThU6z1JjmRyN1vSF8j0bOlGzUwFSMTbctLAdd7QHpeyu0cRiuKrqK1ZTwvQ==", 152 | "cpu": [ 153 | "arm" 154 | ], 155 | "dev": true, 156 | "optional": true, 157 | "os": [ 158 | "linux" 159 | ], 160 | "engines": { 161 | "node": ">=12" 162 | } 163 | }, 164 | "node_modules/@esbuild/linux-arm64": { 165 | "version": "0.19.6", 166 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.6.tgz", 167 | "integrity": "sha512-HQCOrk9XlH3KngASLaBfHpcoYEGUt829A9MyxaI8RMkfRA8SakG6YQEITAuwmtzFdEu5GU4eyhKcpv27dFaOBg==", 168 | "cpu": [ 169 | "arm64" 170 | ], 171 | "dev": true, 172 | "optional": true, 173 | "os": [ 174 | "linux" 175 | ], 176 | "engines": { 177 | "node": ">=12" 178 | } 179 | }, 180 | "node_modules/@esbuild/linux-ia32": { 181 | "version": "0.19.6", 182 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.6.tgz", 183 | "integrity": "sha512-22eOR08zL/OXkmEhxOfshfOGo8P69k8oKHkwkDrUlcB12S/sw/+COM4PhAPT0cAYW/gpqY2uXp3TpjQVJitz7w==", 184 | "cpu": [ 185 | "ia32" 186 | ], 187 | "dev": true, 188 | "optional": true, 189 | "os": [ 190 | "linux" 191 | ], 192 | "engines": { 193 | "node": ">=12" 194 | } 195 | }, 196 | "node_modules/@esbuild/linux-loong64": { 197 | "version": "0.19.6", 198 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.6.tgz", 199 | "integrity": "sha512-82RvaYAh/SUJyjWA8jDpyZCHQjmEggL//sC7F3VKYcBMumQjUL3C5WDl/tJpEiKtt7XrWmgjaLkrk205zfvwTA==", 200 | "cpu": [ 201 | "loong64" 202 | ], 203 | "dev": true, 204 | "optional": true, 205 | "os": [ 206 | "linux" 207 | ], 208 | "engines": { 209 | "node": ">=12" 210 | } 211 | }, 212 | "node_modules/@esbuild/linux-mips64el": { 213 | "version": "0.19.6", 214 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.6.tgz", 215 | "integrity": "sha512-8tvnwyYJpR618vboIv2l8tK2SuK/RqUIGMfMENkeDGo3hsEIrpGldMGYFcWxWeEILe5Fi72zoXLmhZ7PR23oQA==", 216 | "cpu": [ 217 | "mips64el" 218 | ], 219 | "dev": true, 220 | "optional": true, 221 | "os": [ 222 | "linux" 223 | ], 224 | "engines": { 225 | "node": ">=12" 226 | } 227 | }, 228 | "node_modules/@esbuild/linux-ppc64": { 229 | "version": "0.19.6", 230 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.6.tgz", 231 | "integrity": "sha512-Qt+D7xiPajxVNk5tQiEJwhmarNnLPdjXAoA5uWMpbfStZB0+YU6a3CtbWYSy+sgAsnyx4IGZjWsTzBzrvg/fMA==", 232 | "cpu": [ 233 | "ppc64" 234 | ], 235 | "dev": true, 236 | "optional": true, 237 | "os": [ 238 | "linux" 239 | ], 240 | "engines": { 241 | "node": ">=12" 242 | } 243 | }, 244 | "node_modules/@esbuild/linux-riscv64": { 245 | "version": "0.19.6", 246 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.6.tgz", 247 | "integrity": "sha512-lxRdk0iJ9CWYDH1Wpnnnc640ajF4RmQ+w6oHFZmAIYu577meE9Ka/DCtpOrwr9McMY11ocbp4jirgGgCi7Ls/g==", 248 | "cpu": [ 249 | "riscv64" 250 | ], 251 | "dev": true, 252 | "optional": true, 253 | "os": [ 254 | "linux" 255 | ], 256 | "engines": { 257 | "node": ">=12" 258 | } 259 | }, 260 | "node_modules/@esbuild/linux-s390x": { 261 | "version": "0.19.6", 262 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.6.tgz", 263 | "integrity": "sha512-MopyYV39vnfuykHanRWHGRcRC3AwU7b0QY4TI8ISLfAGfK+tMkXyFuyT1epw/lM0pflQlS53JoD22yN83DHZgA==", 264 | "cpu": [ 265 | "s390x" 266 | ], 267 | "dev": true, 268 | "optional": true, 269 | "os": [ 270 | "linux" 271 | ], 272 | "engines": { 273 | "node": ">=12" 274 | } 275 | }, 276 | "node_modules/@esbuild/linux-x64": { 277 | "version": "0.19.6", 278 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.6.tgz", 279 | "integrity": "sha512-UWcieaBzsN8WYbzFF5Jq7QULETPcQvlX7KL4xWGIB54OknXJjBO37sPqk7N82WU13JGWvmDzFBi1weVBajPovg==", 280 | "cpu": [ 281 | "x64" 282 | ], 283 | "dev": true, 284 | "optional": true, 285 | "os": [ 286 | "linux" 287 | ], 288 | "engines": { 289 | "node": ">=12" 290 | } 291 | }, 292 | "node_modules/@esbuild/netbsd-x64": { 293 | "version": "0.19.6", 294 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.6.tgz", 295 | "integrity": "sha512-EpWiLX0fzvZn1wxtLxZrEW+oQED9Pwpnh+w4Ffv8ZLuMhUoqR9q9rL4+qHW8F4Mg5oQEKxAoT0G+8JYNqCiR6g==", 296 | "cpu": [ 297 | "x64" 298 | ], 299 | "dev": true, 300 | "optional": true, 301 | "os": [ 302 | "netbsd" 303 | ], 304 | "engines": { 305 | "node": ">=12" 306 | } 307 | }, 308 | "node_modules/@esbuild/openbsd-x64": { 309 | "version": "0.19.6", 310 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.6.tgz", 311 | "integrity": "sha512-fFqTVEktM1PGs2sLKH4M5mhAVEzGpeZJuasAMRnvDZNCV0Cjvm1Hu35moL2vC0DOrAQjNTvj4zWrol/lwQ8Deg==", 312 | "cpu": [ 313 | "x64" 314 | ], 315 | "dev": true, 316 | "optional": true, 317 | "os": [ 318 | "openbsd" 319 | ], 320 | "engines": { 321 | "node": ">=12" 322 | } 323 | }, 324 | "node_modules/@esbuild/sunos-x64": { 325 | "version": "0.19.6", 326 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.6.tgz", 327 | "integrity": "sha512-M+XIAnBpaNvaVAhbe3uBXtgWyWynSdlww/JNZws0FlMPSBy+EpatPXNIlKAdtbFVII9OpX91ZfMb17TU3JKTBA==", 328 | "cpu": [ 329 | "x64" 330 | ], 331 | "dev": true, 332 | "optional": true, 333 | "os": [ 334 | "sunos" 335 | ], 336 | "engines": { 337 | "node": ">=12" 338 | } 339 | }, 340 | "node_modules/@esbuild/win32-arm64": { 341 | "version": "0.19.6", 342 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.6.tgz", 343 | "integrity": "sha512-2DchFXn7vp/B6Tc2eKdTsLzE0ygqKkNUhUBCNtMx2Llk4POIVMUq5rUYjdcedFlGLeRe1uLCpVvCmE+G8XYybA==", 344 | "cpu": [ 345 | "arm64" 346 | ], 347 | "dev": true, 348 | "optional": true, 349 | "os": [ 350 | "win32" 351 | ], 352 | "engines": { 353 | "node": ">=12" 354 | } 355 | }, 356 | "node_modules/@esbuild/win32-ia32": { 357 | "version": "0.19.6", 358 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.6.tgz", 359 | "integrity": "sha512-PBo/HPDQllyWdjwAVX+Gl2hH0dfBydL97BAH/grHKC8fubqp02aL4S63otZ25q3sBdINtOBbz1qTZQfXbP4VBg==", 360 | "cpu": [ 361 | "ia32" 362 | ], 363 | "dev": true, 364 | "optional": true, 365 | "os": [ 366 | "win32" 367 | ], 368 | "engines": { 369 | "node": ">=12" 370 | } 371 | }, 372 | "node_modules/@esbuild/win32-x64": { 373 | "version": "0.19.6", 374 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.6.tgz", 375 | "integrity": "sha512-OE7yIdbDif2kKfrGa+V0vx/B3FJv2L4KnIiLlvtibPyO9UkgO3rzYE0HhpREo2vmJ1Ixq1zwm9/0er+3VOSZJA==", 376 | "cpu": [ 377 | "x64" 378 | ], 379 | "dev": true, 380 | "optional": true, 381 | "os": [ 382 | "win32" 383 | ], 384 | "engines": { 385 | "node": ">=12" 386 | } 387 | }, 388 | "node_modules/@extism/js-pdk": { 389 | "resolved": "../../crates/core/src/prelude", 390 | "link": true 391 | }, 392 | "node_modules/@types/prop-types": { 393 | "version": "15.7.12", 394 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", 395 | "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", 396 | "dev": true 397 | }, 398 | "node_modules/@types/react": { 399 | "version": "18.3.1", 400 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", 401 | "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", 402 | "dev": true, 403 | "dependencies": { 404 | "@types/prop-types": "*", 405 | "csstype": "^3.0.2" 406 | } 407 | }, 408 | "node_modules/cross-env": { 409 | "version": "7.0.3", 410 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 411 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 412 | "dev": true, 413 | "dependencies": { 414 | "cross-spawn": "^7.0.1" 415 | }, 416 | "bin": { 417 | "cross-env": "src/bin/cross-env.js", 418 | "cross-env-shell": "src/bin/cross-env-shell.js" 419 | }, 420 | "engines": { 421 | "node": ">=10.14", 422 | "npm": ">=6", 423 | "yarn": ">=1" 424 | } 425 | }, 426 | "node_modules/cross-spawn": { 427 | "version": "7.0.3", 428 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 429 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 430 | "dev": true, 431 | "dependencies": { 432 | "path-key": "^3.1.0", 433 | "shebang-command": "^2.0.0", 434 | "which": "^2.0.1" 435 | }, 436 | "engines": { 437 | "node": ">= 8" 438 | } 439 | }, 440 | "node_modules/csstype": { 441 | "version": "3.1.3", 442 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 443 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 444 | "dev": true 445 | }, 446 | "node_modules/esbuild": { 447 | "version": "0.19.6", 448 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.6.tgz", 449 | "integrity": "sha512-Xl7dntjA2OEIvpr9j0DVxxnog2fyTGnyVoQXAMQI6eR3mf9zCQds7VIKUDCotDgE/p4ncTgeRqgX8t5d6oP4Gw==", 450 | "dev": true, 451 | "hasInstallScript": true, 452 | "bin": { 453 | "esbuild": "bin/esbuild" 454 | }, 455 | "engines": { 456 | "node": ">=12" 457 | }, 458 | "optionalDependencies": { 459 | "@esbuild/android-arm": "0.19.6", 460 | "@esbuild/android-arm64": "0.19.6", 461 | "@esbuild/android-x64": "0.19.6", 462 | "@esbuild/darwin-arm64": "0.19.6", 463 | "@esbuild/darwin-x64": "0.19.6", 464 | "@esbuild/freebsd-arm64": "0.19.6", 465 | "@esbuild/freebsd-x64": "0.19.6", 466 | "@esbuild/linux-arm": "0.19.6", 467 | "@esbuild/linux-arm64": "0.19.6", 468 | "@esbuild/linux-ia32": "0.19.6", 469 | "@esbuild/linux-loong64": "0.19.6", 470 | "@esbuild/linux-mips64el": "0.19.6", 471 | "@esbuild/linux-ppc64": "0.19.6", 472 | "@esbuild/linux-riscv64": "0.19.6", 473 | "@esbuild/linux-s390x": "0.19.6", 474 | "@esbuild/linux-x64": "0.19.6", 475 | "@esbuild/netbsd-x64": "0.19.6", 476 | "@esbuild/openbsd-x64": "0.19.6", 477 | "@esbuild/sunos-x64": "0.19.6", 478 | "@esbuild/win32-arm64": "0.19.6", 479 | "@esbuild/win32-ia32": "0.19.6", 480 | "@esbuild/win32-x64": "0.19.6" 481 | } 482 | }, 483 | "node_modules/isexe": { 484 | "version": "2.0.0", 485 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 486 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 487 | "dev": true 488 | }, 489 | "node_modules/js-tokens": { 490 | "version": "4.0.0", 491 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 492 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 493 | }, 494 | "node_modules/loose-envify": { 495 | "version": "1.4.0", 496 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 497 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 498 | "dependencies": { 499 | "js-tokens": "^3.0.0 || ^4.0.0" 500 | }, 501 | "bin": { 502 | "loose-envify": "cli.js" 503 | } 504 | }, 505 | "node_modules/path-key": { 506 | "version": "3.1.1", 507 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 508 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 509 | "dev": true, 510 | "engines": { 511 | "node": ">=8" 512 | } 513 | }, 514 | "node_modules/react": { 515 | "version": "18.3.1", 516 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 517 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 518 | "peer": true, 519 | "dependencies": { 520 | "loose-envify": "^1.1.0" 521 | }, 522 | "engines": { 523 | "node": ">=0.10.0" 524 | } 525 | }, 526 | "node_modules/react-dom": { 527 | "version": "18.3.1", 528 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 529 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 530 | "dependencies": { 531 | "loose-envify": "^1.1.0", 532 | "scheduler": "^0.23.2" 533 | }, 534 | "peerDependencies": { 535 | "react": "^18.3.1" 536 | } 537 | }, 538 | "node_modules/scheduler": { 539 | "version": "0.23.2", 540 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 541 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 542 | "dependencies": { 543 | "loose-envify": "^1.1.0" 544 | } 545 | }, 546 | "node_modules/shebang-command": { 547 | "version": "2.0.0", 548 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 549 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 550 | "dev": true, 551 | "dependencies": { 552 | "shebang-regex": "^3.0.0" 553 | }, 554 | "engines": { 555 | "node": ">=8" 556 | } 557 | }, 558 | "node_modules/shebang-regex": { 559 | "version": "3.0.0", 560 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 561 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 562 | "dev": true, 563 | "engines": { 564 | "node": ">=8" 565 | } 566 | }, 567 | "node_modules/typescript": { 568 | "version": "5.3.2", 569 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", 570 | "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", 571 | "dev": true, 572 | "bin": { 573 | "tsc": "bin/tsc", 574 | "tsserver": "bin/tsserver" 575 | }, 576 | "engines": { 577 | "node": ">=14.17" 578 | } 579 | }, 580 | "node_modules/which": { 581 | "version": "2.0.2", 582 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 583 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 584 | "dev": true, 585 | "dependencies": { 586 | "isexe": "^2.0.0" 587 | }, 588 | "bin": { 589 | "node-which": "bin/node-which" 590 | }, 591 | "engines": { 592 | "node": ">= 8" 593 | } 594 | } 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.tsx", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production node esbuild.js && cross-env ../../target/release/extism-js dist/index.js -i src/index.d.ts -o ../react.wasm" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "BSD-3-Clause", 12 | "devDependencies": { 13 | "@extism/js-pdk": "../../crates/core/src/prelude", 14 | "@types/react": "^18.3.1", 15 | "cross-env": "^7.0.3", 16 | "esbuild": "^0.19.6", 17 | "typescript": "^5.3.2" 18 | }, 19 | "dependencies": { 20 | "react-dom": "^18.3.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function setState(): I32; 3 | export function render(): I32; 4 | } 5 | -------------------------------------------------------------------------------- /examples/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This example shows how to use react and jsx in a plug-in. 3 | * This is a slightly more complex example to demonstrate a common 4 | * pattern for these types of UI plug-ins. 5 | * 6 | * It's helpful to model the plug-in as a state machine. You only need two exports: 7 | * 8 | * + 1. `setState` takes an `Action` which steps or mutates the state machine 9 | * + 2. `render` renders the current state 10 | * 11 | * You can store all the application's state in a single global variable. 12 | * Make sure it can only be mutated through incoming `Action` messages. 13 | * Use JSX to build up your application view and render with `renderToString`. 14 | * 15 | * With this programming model you can build pretty complex plug-ins, like a 16 | * working chat application. 17 | * 18 | * The one downside of course is the in-memory state. However, you could just 19 | * simply offer your plug-in developers a host function to mutate (or persist) 20 | * and fetch it. 21 | * 22 | * This can be tested out with the `shell` command in the extism CLI 23 | * 24 | * $ make compile-examples 25 | * $ extism shell 26 | * 27 | * > extism call examples/react.wasm render --wasi 28 | *

Hello

29 | * > extism call examples/react.wasm setState --input='{"action": "SET_SETTING", "payload": { "backgroundColor": "tomato" }}' --wasi 30 | *

Hello

31 | * > extism call examples/react.wasm render --wasi 32 | *

Hello

33 | */ 34 | import { renderToString } from 'react-dom/server'; 35 | import React from 'react' 36 | 37 | interface Settings { 38 | backgroundColor: string; 39 | } 40 | 41 | // We can store all our application's state here 42 | interface AppState { 43 | settings: Settings; 44 | } 45 | 46 | // We provide a number of "actions" that can be passed to setState to mutate the state machine 47 | enum ActionType { 48 | SetSetting = "SET_SETTING" 49 | } 50 | 51 | interface Action { 52 | action: ActionType; 53 | } 54 | 55 | // We just have one action that can set properties in `settings` 56 | interface SetSettingAction extends Action { 57 | action: ActionType.SetSetting; 58 | payload: Settings; 59 | } 60 | 61 | // Let's just make our application state a global. This should 62 | // be okay as people can only mutate this through setState. 63 | const APP_STATE = { settings: {} } 64 | 65 | // Send an action to this export to step the state machine 66 | // For convenience, this returns the newly rendered view but it could 67 | // return a list of errors or other useful things 68 | export function setState() { 69 | const action: Action = JSON.parse(Host.inputString()) 70 | switch (action.action) { 71 | case ActionType.SetSetting: 72 | const setSettingAction = action as SetSettingAction; 73 | Object.assign(APP_STATE.settings, setSettingAction.payload) 74 | break; 75 | default: 76 | throw new Error(`Uknown action ${action}`) 77 | } 78 | 79 | Host.outputString(renderApp()) 80 | } 81 | 82 | 83 | // Simply render the whole app 84 | // Note: we could accept props here 85 | export function render() { 86 | Host.outputString(renderApp()) 87 | } 88 | 89 | function renderApp() { 90 | const view = ( 91 |
92 |

Hello

93 |
94 | ) 95 | 96 | // use react-dom to render the app component 97 | return renderToString(view) 98 | } 99 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["@extism/js-pdk"], 5 | "noEmit": true 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple_js/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": ["../../crates/core/src/prelude"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple_js/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function goodbye(): I32; 3 | export function greet(): I32; 4 | } 5 | -------------------------------------------------------------------------------- /examples/simple_js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple example of a JavaScript, CJS flavored plug-in: 3 | */ 4 | 5 | function greet() { 6 | Host.outputString(`Hello, ${Host.inputString()}!`); 7 | } 8 | 9 | function goodbye() { 10 | Host.outputString(`Goodbye, ${Host.inputString()}!`); 11 | } 12 | 13 | module.exports = { greet, goodbye }; 14 | -------------------------------------------------------------------------------- /examples/try-catch/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [], 4 | "types": [ 5 | "../../crates/core/src/prelude" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/try-catch/script.d.ts: -------------------------------------------------------------------------------- 1 | declare module "main" { 2 | export function greet(): I32; 3 | } 4 | -------------------------------------------------------------------------------- /examples/try-catch/script.js: -------------------------------------------------------------------------------- 1 | function greet() { 2 | callListeners(); 3 | } 4 | 5 | const LISTENERS = { 6 | hello: async () => { 7 | console.log("inside closure"); 8 | throw new Error("hello world"); 9 | }, 10 | }; 11 | 12 | async function callListeners() { 13 | try { 14 | await LISTENERS["hello"](); 15 | } catch (error) { 16 | console.log("got error", error); 17 | } 18 | } 19 | 20 | module.exports = { greet }; 21 | -------------------------------------------------------------------------------- /install-wasi-sdk.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $7z= "7z" 4 | if (-not (Get-Command $7z -ErrorAction SilentlyContinue)){ 5 | $7z= "$env:Programfiles\7-Zip\7z.exe" 6 | } 7 | $tempPath= "$env:TEMP" 8 | 9 | 10 | if ((Split-Path -Leaf (Get-Location)) -ne "js-pdk") { 11 | Write-Error "Run this inside the root of the js-pdk repo" 12 | exit 1 13 | } 14 | 15 | if ($env:QUICKJS_WASM_SYS_WASI_SDK_PATH) { 16 | if (-Not (Test-Path -Path $env:QUICKJS_WASM_SYS_WASI_SDK_PATH)) { 17 | Write-Error "Download the wasi-sdk to $env:QUICKJS_WASM_SYS_WASI_SDK_PATH" 18 | exit 1 19 | } 20 | exit 0 21 | } 22 | 23 | $PATH_TO_SDK = "wasi-sdk" 24 | if (-Not (Test-Path -Path $PATH_TO_SDK)) { 25 | $VERSION_MAJOR = "24" 26 | $VERSION_MINOR = "0" 27 | $url = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION_MAJOR/wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar.gz" 28 | Invoke-WebRequest -Uri $url -OutFile "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar.gz" 29 | 30 | New-Item -ItemType Directory -Path $PATH_TO_SDK | Out-Null 31 | 32 | & $7z x "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar.gz" -o"$tempPath" | Out-Null 33 | & $7z x -ttar "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar" -o"$tempPath" | Out-Null 34 | 35 | Get-ChildItem -Path "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR" | ForEach-Object { 36 | Move-Item -Path $_.FullName -Destination $PATH_TO_SDK -Force | Out-Null 37 | } 38 | 39 | Remove-Item -Path "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR" -Recurse -Force | Out-Null 40 | Remove-Item -Path "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar" -Recurse -Force | Out-Null 41 | Remove-Item -Path "$tempPath\wasi-sdk-$VERSION_MAJOR.$VERSION_MINOR-mingw.tar.gz" -Recurse -Force | Out-Null 42 | 43 | } 44 | -------------------------------------------------------------------------------- /install-wasi-sdk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -euo pipefail 4 | 5 | if [[ "$(basename $(pwd))" != "js-pdk" ]]; then 6 | echo "Run this inside in the root of the js-pdk repo" 1>&2 7 | exit 1 8 | fi 9 | 10 | # Don't try and install the wasi-sdk if the user has specified the wasi-sdk is installed elsewhere 11 | set +u 12 | if [[ -n "$QUICKJS_WASM_SYS_WASI_SDK_PATH" ]]; then 13 | # Check that something is present where the user says the wasi-sdk is located 14 | if [[ ! -d "$QUICKJS_WASM_SYS_WASI_SDK_PATH" ]]; then 15 | echo "Download the wasi-sdk to $QUICKJS_WASM_SYS_WASI_SDK_PATH" 1>&2 16 | exit 1 17 | fi 18 | exit 0 19 | fi 20 | set -u 21 | 22 | ARCH=`uname -m` 23 | case "$ARCH" in 24 | ix86*|x86_64*) ARCH="x86_64" ;; 25 | arm64*|aarch64*) ARCH="arm64" ;; 26 | *) echo "unknown arch: $ARCH" && exit 1 ;; 27 | esac 28 | 29 | PATH_TO_SDK="wasi-sdk" 30 | if [[ ! -d $PATH_TO_SDK ]]; then 31 | TMPGZ=$(mktemp) 32 | VERSION_MAJOR="24" 33 | VERSION_MINOR="0" 34 | if [[ "$(uname -s)" == "Darwin" ]]; then 35 | curl --fail --location https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${VERSION_MAJOR}/wasi-sdk-${VERSION_MAJOR}.${VERSION_MINOR}-${ARCH}-macos.tar.gz --output $TMPGZ 36 | else 37 | curl --fail --location https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${VERSION_MAJOR}/wasi-sdk-${VERSION_MAJOR}.${VERSION_MINOR}-${ARCH}-linux.tar.gz --output $TMPGZ 38 | fi 39 | mkdir $PATH_TO_SDK 40 | tar xf $TMPGZ -C $PATH_TO_SDK --strip-components=1 41 | fi 42 | -------------------------------------------------------------------------------- /install-windows.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $TAG= "v1.2.0" 4 | $BINARYEN_TAG= "version_116" 5 | $extismPath="$env:Programfiles\Extism" 6 | $binaryenPath="$env:Programfiles\Binaryen" 7 | $7z= "7z" 8 | if (-not (Get-Command $7z -ErrorAction SilentlyContinue)){ 9 | $7z= "$env:Programfiles\7-Zip\7z.exe" 10 | } 11 | $tempPath= "$env:TEMP" 12 | 13 | try { 14 | 15 | $ARCH = (Get-CimInstance Win32_Processor).Architecture 16 | switch ($ARCH) { 17 | 9 { $ARCH = "x86_64" } 18 | 12 { $ARCH = "aarch64" } 19 | default { Write-Host "unknown arch: $ARCH" -ForegroundColor Red; exit 1 } 20 | } 21 | Write-Host "ARCH is $ARCH." 22 | 23 | Write-Host "Downloading extism-js version $TAG." 24 | $TMPGZ = [System.IO.Path]::GetTempFileName() 25 | Invoke-WebRequest -Uri "https://github.com/extism/js-pdk/releases/download/$TAG/extism-js-$ARCH-windows-$TAG.gz" -OutFile "$TMPGZ" 26 | 27 | Write-Host "Installing extism-js." 28 | Remove-Item -Recurse -Path "$extismPath" -Force -ErrorAction SilentlyContinue | Out-Null 29 | New-Item -ItemType Directory -Force -Path $extismPath -ErrorAction Stop | Out-Null 30 | & $7z x "$TMPGZ" -o"$extismPath" >$null 2>&1 31 | 32 | if (-not (Get-Command "wasm-merge" -ErrorAction SilentlyContinue) -or -not (Get-Command "wasm-opt" -ErrorAction SilentlyContinue)) { 33 | 34 | Write-Output "Missing Binaryen tool(s)." 35 | Remove-Item -Recurse -Path "$binaryenPath" -Force -ErrorAction SilentlyContinue | Out-Null 36 | New-Item -ItemType Directory -Force -Path $binaryenPath -ErrorAction Stop | Out-Null 37 | 38 | Write-Output "Downloading Binaryen version $BINARYEN_TAG." 39 | Remove-Item -Recurse -Path "$tempPath\binaryen-*" -Force -ErrorAction SilentlyContinue | Out-Null 40 | Invoke-WebRequest -Uri "https://github.com/WebAssembly/binaryen/releases/download/$BINARYEN_TAG/binaryen-$BINARYEN_TAG-$ARCH-windows.tar.gz" -OutFile "$tempPath\binaryen-$BINARYEN_TAG-$ARCH-windows.tar.gz" 41 | 42 | 43 | Write-Output "Installing Binaryen." 44 | & $7z x "$tempPath\binaryen-$BINARYEN_TAG-$ARCH-windows.tar.gz" -o"$tempPath" >$null 2>&1 45 | & $7z x -ttar "$tempPath\binaryen-$BINARYEN_TAG-$ARCH-windows.tar" -o"$tempPath" >$null 2>&1 46 | Copy-Item "$tempPath\binaryen-$BINARYEN_TAG\bin\wasm-opt.exe" -Destination "$binaryenPath" -ErrorAction Stop | Out-Null 47 | Copy-Item "$tempPath\binaryen-$BINARYEN_TAG\bin\wasm-merge.exe" -Destination "$binaryenPath" -ErrorAction Stop | Out-Null 48 | } 49 | 50 | Write-Output "Install done !" 51 | }catch { 52 | Write-Output "Install Failed: $_.Exception.Message" 53 | exit 1 54 | } 55 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Check if a specific tag was provided 5 | if [ $# -eq 1 ]; then 6 | LATEST_TAG="$1" 7 | echo "Using specified tag: $LATEST_TAG" 8 | else 9 | # Get the latest release 10 | RELEASE_API_URL="https://api.github.com/repos/extism/js-pdk/releases/latest" 11 | response=$(curl -s "$RELEASE_API_URL") 12 | if [ -z "$response" ]; then 13 | echo "Error: Failed to fetch the latest release from GitHub API." 14 | exit 1 15 | fi 16 | 17 | # try to parse tag 18 | LATEST_TAG=$(echo "$response" | grep -m 1 '"tag_name":' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') 19 | 20 | if [ -z "$LATEST_TAG" ]; then 21 | echo "Error: Could not find the latest release tag." 22 | exit 1 23 | fi 24 | 25 | echo "Installing extism-js latest release with tag: $LATEST_TAG" 26 | fi 27 | 28 | OS='' 29 | case `uname` in 30 | Darwin*) OS="macos" ;; 31 | Linux*) OS="linux" ;; 32 | *) echo "unknown os: $OSTYPE" && exit 1 ;; 33 | esac 34 | 35 | ARCH=`uname -m` 36 | case "$ARCH" in 37 | ix86*|x86_64*) ARCH="x86_64" ;; 38 | arm64*|aarch64*) ARCH="aarch64" ;; 39 | *) echo "unknown arch: $ARCH" && exit 1 ;; 40 | esac 41 | 42 | BINARYEN_TAG="version_116" 43 | DOWNLOAD_URL="https://github.com/extism/js-pdk/releases/download/$LATEST_TAG/extism-js-$ARCH-$OS-$LATEST_TAG.gz" 44 | 45 | # Function to check if a directory is in PATH and writable 46 | is_valid_install_dir() { 47 | [[ ":$PATH:" == *":$1:"* ]] && [ -w "$1" ] 48 | } 49 | 50 | INSTALL_DIR="" 51 | USE_SUDO="" 52 | 53 | # Check for common user-writable directories in PATH 54 | for dir in "$HOME/bin" "$HOME/.local/bin" "$HOME/.bin"; do 55 | if is_valid_install_dir "$dir"; then 56 | INSTALL_DIR="$dir" 57 | break 58 | fi 59 | done 60 | 61 | # If no user-writable directory found, use system directory 62 | if [ -z "$INSTALL_DIR" ]; then 63 | INSTALL_DIR="/usr/local/bin" 64 | USE_SUDO=1 65 | fi 66 | 67 | echo "Checking for binaryen..." 68 | 69 | if ! which "wasm-merge" > /dev/null || ! which "wasm-opt" > /dev/null; then 70 | echo "Missing binaryen tool(s)" 71 | 72 | # binaryen use arm64 instead where as extism-js uses aarch64 for release file naming 73 | case "$ARCH" in 74 | aarch64*) ARCH="arm64" ;; 75 | esac 76 | 77 | # matches the case where the user installs extism-pdk in a Linux-based Docker image running on mac m1 78 | # binaryen didn't have arm64 release file for linux 79 | if [ $ARCH = "arm64" ] && [ $OS = "linux" ]; then 80 | ARCH="x86_64" 81 | fi 82 | 83 | if [ $OS = "macos" ]; then 84 | echo "Installing binaryen and wasm-merge using homebrew" 85 | brew install binaryen 86 | else 87 | if [ ! -e "binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" ]; then 88 | echo 'Downloading binaryen...' 89 | curl -L -O "https://github.com/WebAssembly/binaryen/releases/download/$BINARYEN_TAG/binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" 90 | fi 91 | rm -rf 'binaryen' "binaryen-$BINARYEN_TAG" 92 | tar xf "binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" 93 | mv "binaryen-$BINARYEN_TAG"/ binaryen/ 94 | sudo mkdir -p /usr/local/binaryen 95 | if ! which 'wasm-merge' > /dev/null; then 96 | echo "Installing wasm-merge..." 97 | rm -f /usr/local/binaryen/wasm-merge 98 | sudo mv binaryen/bin/wasm-merge /usr/local/binaryen/wasm-merge 99 | sudo ln -s /usr/local/binaryen/wasm-merge /usr/local/bin/wasm-merge 100 | else 101 | echo "wasm-merge is already installed" 102 | fi 103 | if ! which 'wasm-opt' > /dev/null; then 104 | echo "Installing wasm-opt..." 105 | rm -f /usr/local/bin/wasm-opt 106 | sudo mv binaryen/bin/wasm-opt /usr/local/binaryen/wasm-opt 107 | sudo ln -s /usr/local/binaryen/wasm-opt /usr/local/bin/wasm-opt 108 | else 109 | echo "wasm-opt is already installed" 110 | fi 111 | fi 112 | else 113 | echo "binaryen tools are already installed" 114 | fi 115 | 116 | TARGET="$INSTALL_DIR/extism-js" 117 | echo "Downloading extism-js from: $DOWNLOAD_URL" 118 | 119 | if curl -fsSL --output /tmp/extism-js.gz "$DOWNLOAD_URL"; then 120 | gunzip /tmp/extism-js.gz 121 | 122 | if [ "$USE_SUDO" = "1" ]; then 123 | echo "No user-writable bin directory found in PATH. Using sudo to install in $INSTALL_DIR" 124 | sudo mv /tmp/extism-js "$TARGET" 125 | else 126 | mv /tmp/extism-js "$TARGET" 127 | fi 128 | chmod +x "$TARGET" 129 | 130 | echo "Successfully installed extism-js to $TARGET" 131 | else 132 | echo "Failed to download or install extism-js. Curl exit code: $?" 133 | exit 134 | fi 135 | 136 | # Warn the user if the chosen path is not in the path 137 | if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then 138 | echo "Note: $INSTALL_DIR is not in your PATH. You may need to add it to your PATH or use the full path to run extism-js." 139 | fi 140 | 141 | echo "Installation complete. Try to run 'extism-js --version' to ensure it was correctly installed." --------------------------------------------------------------------------------