{"id":13774178,"url":"https://github.com/extism/js-pdk","last_synced_at":"2025-04-06T19:13:05.041Z","repository":{"id":139444042,"uuid":"588293906","full_name":"extism/js-pdk","owner":"extism","description":"Write Extism plugins in JavaScript \u0026 TypeScript","archived":false,"fork":false,"pushed_at":"2025-03-24T21:33:03.000Z","size":3264,"stargazers_count":71,"open_issues_count":16,"forks_count":15,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-30T18:08:25.180Z","etag":null,"topics":["extism","javascript","plugins","typescript","wasm"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/extism.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-12T19:33:49.000Z","updated_at":"2025-03-30T16:17:24.000Z","dependencies_parsed_at":"2024-01-03T18:28:40.526Z","dependency_job_id":"edd5d27a-d42e-4321-863e-7a227e24d04b","html_url":"https://github.com/extism/js-pdk","commit_stats":{"total_commits":254,"total_committers":17,"mean_commits":"14.941176470588236","dds":0.3228346456692913,"last_synced_commit":"d9a2a37d2d85328f77ba5446fbc6a7d89c769746"},"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fjs-pdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fjs-pdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fjs-pdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fjs-pdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/extism","download_url":"https://codeload.github.com/extism/js-pdk/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247535519,"owners_count":20954576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["extism","javascript","plugins","typescript","wasm"],"created_at":"2024-08-03T17:01:24.385Z","updated_at":"2025-04-06T19:13:05.018Z","avatar_url":"https://github.com/extism.png","language":"Rust","funding_links":[],"categories":["\u003ca name=\"extism\"\u003e\u003c/a\u003e[Extism](https://github.com/extism/extism) \u003csup\u003e[top⇈](#contents)\u003c/sup\u003e","Rust"],"sub_categories":[],"readme":"# Extism JavaScript PDK\n\n![GitHub License](https://img.shields.io/github/license/extism/extism)\n![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/js-pdk)\n\nThis project contains a tool that can be used to create\n[Extism Plug-ins](https://extism.org/docs/concepts/plug-in) in JavaScript.\n\n## Overview\n\nThis PDK uses [QuickJS](https://bellard.org/quickjs/) and\n[wizer](https://github.com/bytecodealliance/wizer) to run javascript as an\nExtism Plug-in.\n\nThis is essentially a fork of [Javy](https://github.com/bytecodealliance/javy)\nby Shopify. We may wish to collaborate and upstream some things to them. For the\ntime being I built this up from scratch using some of their crates, namely\nquickjs-wasm-rs.\n\n\u003e Warning: This is a very bare-bones runtime. It's only for running pure JS code\n\u003e and it does not expose node APIs or the browser APIs. We have limited support\n\u003e for some W3C APIs (e.g. we support `Text{Encoder,Decoder}` but not `fetch`),\n\u003e but many modules you take from npm will not work out of the box. There is no\n\u003e support for node APIs or anything that makes syscalls typically. You'll need\n\u003e to polyfill any APIs with a pure JS implementation, which is often possible,\n\u003e but some things, such as controlling sockets, are not possible. Feel free to\n\u003e [file an issue](https://github.com/extism/js-pdk/issues/new) if you think an\n\u003e API should be supported though.\n\n## Install the compiler\n\nWe release the compiler as native binaries you can download and run. Check the\n[releases](https://github.com/extism/js-pdk/releases) page for the latest.\n\n## Install Script\n\n### Linux, macOS\n\n```bash\ncurl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh\nbash install.sh\n```\n\n### Windows\n\n\u003e 7zip is required, you can find it [here](https://www.7-zip.org/).\n\nOpen the Command Prompt as Administrator, then run :\n\n```bash\npowershell Invoke-WebRequest -Uri https://raw.githubusercontent.com/extism/js-pdk/main/install-windows.ps1 -OutFile install-windows.ps1\npowershell -executionpolicy bypass -File .\\install-windows.ps1\n```\n\nThis will install extism-js and binaryen dependency under `Program File` folder\n(i.e. C:\\Program Files\\Binaryen and C:\\Program Files\\Extism). You must add these\npaths to your PATH environment variable.\n\n### Testing the Install\n\n\u003e _Note_: [Binaryen](https://github.com/WebAssembly/binaryen), specifically the\n\u003e `wasm-merge` and `wasm-opt` tools are required as a dependency. We will try to\n\u003e package this up eventually but for now it must be reachable on your machine.\n\u003e You can install on mac with `brew install binaryen` or see their\n\u003e [releases page](https://github.com/WebAssembly/binaryen/releases).\n\nThen run command with `-h` to see the help:\n\n```\nextism-js 1.1.1\nExtism JavaScript PDK Plugin Compiler\n\nUSAGE:\n    extism-js [FLAGS] [OPTIONS] \u003cinput-js\u003e\n\nFLAGS:\n    -h, --help        Prints help information\n        --skip-opt    Skip final optimization pass\n    -V, --version     Prints version information\n\nOPTIONS:\n    -i \u003cinterface-file\u003e         [default: index.d.ts]\n    -o \u003coutput\u003e                 [default: index.wasm]\n\nARGS:\n    \u003cinput-js\u003e\n```\n\n\u003e **Note**: If you are using mac, you may need to tell your security system this\n\u003e unsigned binary is fine. If you think this is dangerous, or can't get it to\n\u003e work, see the \"compile from source\" section below.\n\n## Getting Started\n\nThe goal of writing an\n[Extism plug-in](https://extism.org/docs/concepts/plug-in) is to compile your\nJavaScript code to a Wasm module with exported functions that the host\napplication can invoke. The first thing you should understand is creating an\nexport.\n\n### Exports\n\nLet's write a simple program that exports a `greet` function which will take a\nname as a string and return a greeting string. Paste this into a file\n`plugin.js`:\n\n```javascript\nfunction greet() {\n  const name = Host.inputString();\n  Host.outputString(`Hello, ${name}`);\n}\n\nmodule.exports = { greet };\n```\n\nSome things to note about this code:\n\n1. We can export functions by name using the normal `module.exports` object.\n   This allows the host to invoke this function. Like a normal js module,\n   functions cannot be seen from the outside without exporting them.\n2. Currently, you must use\n   [CJS Module syntax](https://nodejs.org/api/modules.html#modules-commonjs-modules)\n   when not using a bundler. So the `export` keyword is not directly supported.\n   See the [Using with a Bundler](#using-with-a-bundler) section for more.\n3. In this PDK we code directly to the ABI. We get input from the using using\n   `Host.input*` functions and we return data back with the `Host.output*`\n   functions.\n\nWe must also describe the Wasm interface for our plug-in. We do this with a\ntypescript module DTS file. Here is our `plugin.d.ts` file:\n\n```typescript\ndeclare module \"main\" {\n  // Extism exports take no params and return an I32\n  export function greet(): I32;\n}\n```\n\nLet's compile this to Wasm now using the `extism-js` tool:\n\n```bash\nextism-js plugin.js -i plugin.d.ts -o plugin.wasm\n```\n\nWe can now test `plugin.wasm` using the\n[Extism CLI](https://github.com/extism/cli)'s `run` command:\n\n```bash\nextism call plugin.wasm greet --input=\"Benjamin\" --wasi\n# =\u003e Hello, Benjamin!\n```\n\n\u003e **Note**: Currently `wasi` must be provided for all JavaScript plug-ins even\n\u003e if they don't need system access, however we're looking at how to make this\n\u003e optional.\n\n\u003e **Note**: We also have a web-based, plug-in tester called the\n\u003e [Extism Playground](https://playground.extism.org/)\n\n### More Exports: Error Handling\n\nWe catch any exceptions thrown and return them as errors to the host. Suppose we\nwant to re-write our greeting module to never greet Benjamins:\n\n```javascript\nfunction greet() {\n  const name = Host.inputString();\n  if (name === \"Benjamin\") {\n    throw new Error(\"Sorry, we don't greet Benjamins!\");\n  }\n  Host.outputString(`Hello, ${name}!`);\n}\n\nmodule.exports = { greet };\n```\n\nNow compile and run:\n\n```bash\nextism-js plugin.js -i plugin.d.ts -o plugin.wasm\nextism call plugin.wasm greet --input=\"Benjamin\" --wasi\n# =\u003e Error: Uncaught Error: Sorry, we don't greet Benjamins!\n# =\u003e    at greet (script.js:4)\n# =\u003e    at \u003ceval\u003e (script.js)\necho $? # print last status code\n# =\u003e 1\nextism call plugin.wasm greet --input=\"Zach\" --wasi\n# =\u003e Hello, Zach!\necho $?\n# =\u003e 0\n```\n\n### JSON\n\nIf you want to handle more complex types, the plug-in can input and output bytes\nwith `Host.inputBytes` and `Host.outputBytes` respectively. Those bytes can\nrepresent any complex type. A common format to use is JSON:\n\n```javascript\nfunction sum() {\n  const params = JSON.parse(Host.inputString());\n  Host.outputString(JSON.stringify({ sum: params.a + params.b }));\n}\n```\n\n```bash\nextism call plugin.wasm sum --input='{\"a\": 20, \"b\": 21}' --wasi\n# =\u003e {\"sum\":41}\n```\n\n### Configs\n\nConfigs are key-value pairs that can be passed in by the host when creating a\nplug-in. These can be useful to statically configure the plug-in with some data\nthat exists across every function call. Here is a trivial example using\n`Config.get`:\n\n```javascript\nfunction greet() {\n  const user = Config.get(\"user\");\n  Host.outputString(`Hello, ${user}!`);\n}\n\nmodule.exports = { greet };\n```\n\nTo test it, the [Extism CLI](https://github.com/extism/cli) has a `--config`\noption that lets you pass in `key=value` pairs:\n\n```bash\nextism call plugin.wasm greet --config user=Benjamin --wasi\n# =\u003e Hello, Benjamin!\n```\n\n### Variables\n\nVariables are another key-value mechanism but it's a mutable data store that\nwill persist across function calls. These variables will persist as long as the\nhost has loaded and not freed the plug-in. You can use `Var.getBytes`,\n`Var.getString`, and `Var.set` to manipulate vars:\n\n```javascript\nfunction count() {\n  let count = Var.getString(\"count\") || \"0\";\n  count = parseInt(count, 10);\n  count += 1;\n  Var.set(\"count\", count.toString());\n  Host.outputString(count.toString());\n}\n\nmodule.exports = { count };\n```\n\n### Logging\n\nAt the current time, calling `console.log` emits an `info` log. Please file an\nissue or PR if you want to expose the raw logging interface:\n\n```javascript\nfunction logStuff() {\n  console.log(\"Hello, World!\");\n}\n\nmodule.exports = { logStuff };\n```\n\nRunning it, you need to pass a log-level flag:\n\n```\nextism call plugin.wasm logStuff --wasi --log-level=info\n# =\u003e 2023/10/17 14:25:00 Hello, World!\n```\n\n### HTTP\n\nHTTP calls can be made using the synchronous API `Http.request`:\n\n```javascript\nfunction callHttp() {\n  const request = {\n    method: \"GET\",\n    url: \"https://jsonplaceholder.typicode.com/todos/1\",\n  };\n  const response = Http.request(request);\n  if (response.status != 200) {\n    throw new Error(`Got non 200 response ${response.status}`);\n  }\n  Host.outputString(response.body);\n}\n\nmodule.exports = { callHttp };\n```\n\n### Host Functions\n\nUntil the js-pdk hits 1.0, we may make changes to this API. To use host\nfunctions you need to declare a TypeScript interface `extism:host/user`:\n\n```typescript\ndeclare module \"main\" {\n  export function greet(): I32;\n}\n\ndeclare module \"extism:host\" {\n  interface user {\n    myHostFunction1(ptr: I64): I64;\n    myHostFunction2(ptr: I64): I64;\n  }\n}\n```\n\n**Note:** These functions may only use `I64` arguments, up to 5 arguments.\n\nTo use these you need to use `Host.getFunctions()`:\n\n```typescript\nconst { myHostFunction1, myHostFunction2 } = Host.getFunctions();\n```\n\nCalling them is a similar process to other PDKs. You need to manage the memory\nwith the Memory object and pass across an offset as the `I64` ptr. Using the\nreturn value means dereferencing the returned `I64` ptr from Memory.\n\n```typescript\nfunction greet() {\n  let msg = \"Hello from js 1\";\n  let mem = Memory.fromString(msg);\n  let offset = myHostFunction1(mem.offset);\n  let response = Memory.find(offset).readString();\n  if (response != \"myHostFunction1: \" + msg) {\n    throw Error(`wrong message came back from myHostFunction1: ${response}`);\n  }\n\n  msg = { hello: \"world!\" };\n  mem = Memory.fromJsonObject(msg);\n  offset = myHostFunction2(mem.offset);\n  response = Memory.find(offset).readJsonObject();\n  if (response.hello != \"myHostFunction2\") {\n    throw Error(`wrong message came back from myHostFunction2: ${response}`);\n  }\n\n  Host.outputString(`Hello, World!`);\n}\n\nmodule.exports = { greet };\n```\n\n**IMPORTANT:** Currently, a limitation in the js-pdk is that host functions may\nonly have up to 5 arguments.\n\n## Using with a bundler\n\nThe compiler cli and core engine can now run bundled code. You will want to use\na bundler if you want to want to or include modules from NPM, or write the\nplugin in Typescript, for example.\n\nThere are 2 primary constraints to using a bundler:\n\n1. Your compiled output must be CJS format, not ESM\n2. You must target es2020 or lower\n\n### Using with esbuild\n\nThe easiest way to set this up would be to use esbuild. The following is a\nquickstart guide to setting up a project:\n\n```bash\n# Make a new JS project\nmkdir extism-plugin\ncd extism-plugin\nnpm init -y\nnpm install esbuild @extism/js-pdk --save-dev\nmkdir src\nmkdir dist\n```\n\nOptionally add a `jsconfig.json` or `tsconfig.json` to improve intellisense:\n\n```jsonc\n{\n  \"compilerOptions\": {\n    \"lib\": [], // this ensures unsupported globals aren't suggested\n    \"types\": [\"@extism/js-pdk\"], // while this makes the IDE aware of the ones that are\n    \"noEmit\": true // this is only relevant for tsconfig.json\n  },\n  \"include\": [\"src/**/*\"]\n}\n```\n\nAdd `esbuild.js`:\n\n```js\nconst esbuild = require(\"esbuild\");\n// include this if you need some node support:\n// npm i @esbuild-plugins/node-modules-polyfill --save-dev\n// const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill')\n\nesbuild\n  .build({\n    // supports other types like js or ts\n    entryPoints: [\"src/index.js\"],\n    outdir: \"dist\",\n    bundle: true,\n    sourcemap: true,\n    //plugins: [NodeModulesPolyfillPlugin()], // include this if you need some node support\n    minify: false, // might want to use true for production build\n    format: \"cjs\", // needs to be CJS for now\n    target: [\"es2020\"], // don't go over es2020 because quickjs doesn't support it\n  });\n```\n\nAdd a `build` script to your `package.json`:\n\n```json\n{\n  \"name\": \"extism-plugin\",\n  // ...\n  \"scripts\": {\n    // ...\n    \"build\": \"node esbuild.js \u0026\u0026 extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm\"\n  }\n  // ...\n}\n```\n\nLet's import a module from NPM:\n\n```bash\nnpm install --save fastest-levenshtein\n```\n\nNow make some code in `src/index.js`. You can use `import` to load node_modules:\n\n\u003e **Note**: This module uses the ESM Module syntax. The bundler will transform\n\u003e all the code to CJS for us\n\n```js\nimport { closest, distance } from \"fastest-levenshtein\";\n\n// this function is private to the module\nfunction privateFunc() {\n  return \"world\";\n}\n\n// use any export syntax to export a function be callable by the extism host\nexport function get_closest() {\n  let input = Host.inputString();\n  let result = closest(input, [\"slow\", \"faster\", \"fastest\"]);\n  Host.outputString(result + \" \" + privateFunc());\n}\n```\n\nAnd a d.ts file for it at `src/index.d.ts`:\n\n```typescript\ndeclare module \"main\" {\n  // Extism exports take no params and return an I32\n  export function get_closest(): I32;\n}\n```\n\n```bash\n# Run the build script and the plugin will be compiled to dist/plugin.wasm\nnpm run build\n# You can now call from the extism cli or a host SDK\nextism call dist/plugin.wasm get_closest --input=\"fest\" --wasi\n# =\u003e faster World\n```\n\n## Using with React and JSX / TSX\n\nOftentimes people want their JS plug-ins to control or create views. React and\nJSX are a great way to do this. Here is the simplest example. Let's just render\na simple view in a typescript plugin.\n\nFirst declare a `render` export:\n\n```typescript\ndeclare module \"main\" {\n  export function render(): I32;\n}\n```\n\nNow install the deps:\n\n```bash\nnpm install react-dom --save\nnpm install @types/react --save-dev\n```\n\nNow we can make an index.tsx:\n\n```typescript\nimport { renderToString } from \"react-dom/server\";\nimport React from \"react\";\n\ninterface AppProps {\n  name: string;\n}\n\nfunction App(props: AppProps) {\n  return (\n    \u003c\u003e\n      \u003cp\u003eHello ${props.name}!\u003c/p\u003e\n    \u003c/\u003e\n  );\n}\n\nexport function render() {\n  const props = JSON.parse(Host.inputString()) as AppProps;\n  const app = \u003cApp {...props} /\u003e;\n  Host.outputString(renderToString(app));\n}\n```\n\nTo see a more complex example of how you might build something real, see\n[examples/react](./examples/react/)\n\n## Generating Bindings\n\nIt's often very useful to define a schema to describe the function signatures\nand types you want to use between Extism SDK and PDK languages.\n\n[XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source\nframework to generate PDK bindings for Extism plug-ins. It's used by the\n[XTP Platform](https://www.getxtp.com/), but can be used outside of the platform\nto define any Extism compatible plug-in system.\n\n### 1. Install the `xtp` CLI.\n\nSee installation instructions\n[here](https://docs.xtp.dylibso.com/docs/cli#installation).\n\n### 2. Create a schema using our OpenAPI-inspired IDL:\n\n```yaml\nversion: v1-draft\nexports: \n  CountVowels:\n      input: \n          type: string\n          contentType: text/plain; charset=utf-8\n      output:\n          $ref: \"#/components/schemas/VowelReport\"\n          contentType: application/json\n# components.schemas defined in example-schema.yaml...\n```\n\n\u003e See an example in [example-schema.yaml](./example-schema.yaml), or a full\n\u003e \"kitchen sink\" example on\n\u003e [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/).\n\n### 3. Generate bindings to use from your plugins:\n\n```\nxtp plugin init --schema-file ./example-schema.yaml\n  \u003e 1. TypeScript                      \n    2. Go                              \n    3. Rust                            \n    4. Python                          \n    5. C#                              \n    6. Zig                             \n    7. C++                             \n    8. GitHub Template                 \n    9. Local Template\n```\n\nThis will create an entire boilerplate plugin project for you to get started\nwith:\n\n```typescript\n/**\n * @returns {VowelReport} The result of counting vowels on the Vowels input.\n */\nexport function CountVowelsImpl(input: string): VowelReport {\n  // TODO: fill out your implementation here\n  throw new Error(\"Function not implemented.\");\n}\n```\n\nImplement the empty function(s), and run `xtp plugin build` to compile your\nplugin.\n\n\u003e For more information about XTP Bindgen, see the\n\u003e [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and\n\u003e the official\n\u003e [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema).\n\n## Compiling the compiler from source\n\n### Prerequisites\n\nBefore compiling the compiler, you need to install prerequisites.\n\n1. Install Rust using [rustup](https://rustup.rs)\n2. Install the WASI target platform via\n   `rustup target add --toolchain stable wasm32-wasip1`\n3. Install the wasi sdk using the makefile command: `make download-wasi-sdk`\n4. Install [CMake](https://cmake.org/install/) (on macOS with homebrew,\n   `brew install cmake`)\n5. Install [Binaryen](https://github.com/WebAssembly/binaryen/) and add it's\n   install location to your PATH (only wasm-opt is required for build process)\n6. Install [7zip](https://www.7-zip.org/)(only for Windows)\n\n### Compiling from source\n\nRun make to compile the core crate (the engine) and the cli:\n\n```\nmake\n```\n\nTo test the built compiler (ensure you have Extism installed):\n\n```bash\n./target/release/extism-js bundle.js -i bundle.d.ts -o out.wasm\nextism call out.wasm count_vowels --wasi --input='Hello World Test!'\n# =\u003e \"{\\\"count\\\":4}\"\n```\n\n## How it works\n\nThis works a little differently than other PDKs. You cannot compile JS to Wasm\nbecause it doesn't have an appropriate type system to do this. Something like\n[Assemblyscript](https://github.com/extism/assemblyscript-pdk) is better suited\nfor this. Instead, we have compiled QuickJS to Wasm. The `extism-js` command we\nhave provided here is a little compiler / wrapper that does a series of things\nfor you:\n\n1. It loads an \"engine\" Wasm program containing the QuickJS runtime\n2. It initializes a QuickJS context\n3. It loads your js source code into memory\n4. It parses the js source code for exports and generates 1-to-1 proxy export\n   functions in Wasm\n5. It freezes and emits the machine state as a new Wasm file at this\n   post-initialized point in time\n\nThis new Wasm file can be used just like any other Extism plugin.\n\n## Why not use Javy?\n\nJavy, and many other high level language Wasm tools, assume use of the _command\npattern_. This is when the Wasm module only exports a main function and\ncommunicates with the host through stdin and stdout. With Extism, we have more\nof a shared library interface. The module exposes multiple entry points through\nexported functions. Furthermore, Javy has many Javy and Shopify specific things\nit's doing that we will not need. However, the core idea is the same, and we can\npossibly contribute by adding support to Javy for non-command-pattern modules.\nThen separating the Extism PDK specific stuff into another repo.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextism%2Fjs-pdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fextism%2Fjs-pdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextism%2Fjs-pdk/lists"}