extism / js-pdk

Write Extism plugins in JavaScript & TypeScript
42 stars 16 forks source link

feat: Support bundled JS applications #1

Closed bhelx closed 1 year ago

bhelx commented 1 year ago

Instead of parsing and statically looking for module exports in the compile step, this evaluates the code to find the exports. Thus we can support bundled JS code.

How to use

In order to get this to work, you just need a working bundler and you need to output in CJS format, not ESM format. I've tested in esbuild but webpack and other should work. You should be able to write your plugin in typescript too, i haven't tested though!

Here is an example of a js project from scratch with esbuild:

# Make a new JS project
mkdir extism-plugin
cd extism-plugin
npm init -y
npm install --save-dev

Add esbuild.js:

const esbuild = require('esbuild');

esbuild
    .build({
        entryPoints: ['src/index.js'],
        outdir: 'dist',
        bundle: true,
        sourcemap: true,
        minify: false,
        format: 'cjs', // needs to be CJS for now
        target: ['es2020'] // don't go over es2020 because quickjs doesn't support it
    })

make some directories

mkdir src
mkdir dist

Add a build script to your package.json:

{
  "name": "extism-plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "node esbuild.js && extism-js dist/index.js -o dist/plugin.wasm"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.17.2"
  }
}

Let's import a module from NPM:

npm install --save fastest-levenshtein

Now make some code in src/index.js. You can use import to load node_modules:

import {distance, closest} from 'fastest-levenshtein'

// this function is private to the module
function privateFunc() { return 'world' }

// use any export syntax to export a function be callable by the extism host
export function get_closest() {
  let input = Host.inputString()
  let result = closest(input, ['slow', 'faster', 'fastest'])
  Host.outputString(result + ' ' + privateFunc())
  return 0
}
# Run the build script and the plugin will be compiled to dist/plugin.wasm
npm run build
# You can now call from the extism cli or a host SDK
extism call dist/plugin.wasm get_closest --input="fest" --wasi
faster World                     

Next Steps

I need to go back and possibly refactor the core after this. Right now it seems to still work but I'm concerned the global functions will get their names mangled. So instead core should look inside module.exports and find the function reference there rather than expecting the global name to be preserved.