Deno
Deno provides an API to run JavaScript/TypeScript in non-browser environment with more access to system resources, but still within a sandbox.
API and Permissions
All Deno permissions must be explicitly declared in package.json
.
DenoCommand
works similarly to a regular shell Command. It can be created with shell.createDenoCommand
.
The returned DenoCommand
object has execute
and spawn
methods (just like regular Command
).
To run a Deno command, you need one of the the following scoped permissions:
shell:deno:execute
shell:deno:spawn
Read sample code for more details.
RPC Library Style API
If you’re looking to run Deno as a library for resource-intensive tasks or those requiring system access, the Deno API offers a solution. You can execute Deno scripts using
shell.createDenoCommand
, passing input throughargv
andstdin
, and retrieving results viastdout
andstderr
. This approach essentially treats Deno like a CLI, as illustrated in an example (Transform Image with Sharp) provided below. However, using a CLI is not always ideal for those who prefer calling scripts directly like a typical library. The@kksh/api
solves this by offering an abstraction in RPC style, allowing Deno scripts to be invoked as libraries rather than CLIs.
The shell API provides a function createDenoRpcChannel
that creates a Deno RPC API.
function createDenoRpcChannel<LocalAPI extends {}, RemoteAPI extends {}>( scriptPath: string, args: string[], config: Partial<DenoRunConfig> & SpawnOptions, localAPIImplementation: LocalAPI): Promise<RPCChannel<LocalAPI, RemoteAPI>> // ...}
Then you can call the remote API functions directly.
- Since this is a bidirectional IPC channel, you can pass
LocalAPI
andRemoteAPI
generics to define the API.LocalAPI
is what you want to expose to the other side of the channel, andRemoteAPI
is what you want to call from the other side.
args
areargv
like regular commandsconfig
is the same as config inshell.createDenoCommand
, used for configuring deno permissions and other options.localAPIImplementation
is the implementation of the API you want to expose, pass{}
if you don’t want to expose any API.
Debug Log in Deno
To show stderr
in the console, you have to manually listen to the stderr
event.
const { rpcChannel, process, command } = await shell.createDenoRpcChannel<object, API>( '$EXTENSION/deno-src/index.ts', [], {}, {});command.stderr.on('data', (data: string) => { console.warn(data);});
Here I used console.warn
to display the deno log in a different color.
Sample Code
Math Library (RPC Style)
Suppose we want to build a math library in Deno and call it from extension running in web worker or iframe.
Here is how the library will be written in Deno. Just provide an API object.
export interface MathLibAPI { add(a: number, b: number): Promise<number> subtract(a: number, b: number): Promise<number>}
Deno Script
I recommend use a separate subdirectory for Deno scripts to avoid confusion with regular extension code. I will use deno-src
in this example.
deno init deno-srcdeno add jsr:@kunkun/api
This will create a import map in deno.json
{ "imports": { "@kunkun/api": "jsr:@kunkun/api@^x.x.x" // Use the latest version }}
Kunkun’s API package for Deno is on JSR, https://jsr.io/@kunkun/api
Note: the scope is @kunkun
rather than @kksh
. This is because @kunkun
scope was taken on npm registry.
expose
function is provided by @kunkun/api/runtime/deno
to expose the API object to its parent process.
import { expose } from '@kunkun/api/runtime/deno'import { type MathLibAPI } from "../types.ts"
// Define your API methodsexport const mathLib: MathLibAPI = { add: async (a: number, b: number) => a + b, subtract: async (a: number, b: number) => a - b}
expose(apiMethods)
expose()
is a wrapper function for building a bidirectional RPC channel. It returns the RPCChannel
object,
which can be used to call API exposed from the other side of the channel.
Manual Channel Configuration
You shouldn’t need to worry about how the channel is created, but here is how it’s done manually.
It’s based on @kunkun/kkrpc
package, see https://jsr.io/@kunkun/kkrpc
import { DenoStdio, RPCChannel } from "@kunkun/kkrpc"import { type MathLibAPI } from "../types.ts"
// Define your API methods (that will be exposed to the other side)export const mathLib: MathLibAPI = { add: async (a: number, b: number) => a + b, subtract: async (a: number, b: number) => a - b}
const stdio = new DenoStdio(Deno.stdin.readable, Deno.stdout.writable)const channel = new RPCChannel<MathLibAPI, {}>(stdio, mathLib)const api = channel.getApi()
Extension Code
In main extension code, use shell.createDenoRpcChannel
to start running a Deno script and get the API object.
import { deno } from "@kksh/api/ui/template";
import { deno } from "@kksh/api/ui/custom";
import { deno } from "@kksh/api/headless";
import { type MathLibAPI } from "./types.ts"
// ...// First generic is Local API interface to expose to the other side of the channel. We set it to {} to not expose any API. Last function parameter is the implementation of the API, which we also set to {}const { rpcChannel, process } = await shell.createDenoRpcChannel< {}, MathLibAPI>("$EXTENSION/deno-src/math-lib.ts", [], { allowRead: ["$DESKTOP/**"] // this math lib doesn't need this permission, it's just an example to show how to pass permissions}, {});
const api = rpcChannel.getApi();await api.add(1, 2).then(console.log); // 3await api.subtract(1, 2).then(console.log); // -1
await process.kill() // please always remember to kill the Deno process, or it may run forever
Permissions
Using Deno as a CLI requires shell:deno:execute
permission.
Using Deno as a library requires shell:deno:spawn
permission because it will be a long running process.
"shell:stdin-write"
is required for shell.createDenoRpcChannel
to work, because our kkrpc
protocol uses stdin
and stdout
to communicate.
"shell:kill"
is required to kill the Deno process when you are done. (Although when extension quits, it will auto kill all leftover processes. But it’s still a good practice to kill it manually.)
{ ... "permissions": [ { "permission": "shell:deno:spawn", "allow": [ { "path": "$EXTENSION/deno-src/index.ts", "read": ["$DESKTOP"] } ] }, "shell:stdin-write", "shell:kill" ] ...}
Transform Image with Sharp (CLI Style)
In this sample, we will transform an image with sharp, which is not runnable in browser.
Suppose we have a template worker extension project with the following structure:
Directorysrc
- deno-script.ts
- index.ts
- …
index.ts
is the entrypoint to the extension command, deno-script.ts
is a script that will be executed by Deno.
import { parseArgs } from "jsr:@std/cli/parse-args"import sharp from "npm:sharp"
const args = parseArgs(Deno.args)console.log(args)
const input = args.iconst output = args.o
sharp(input).blur(10).resize(300, 300).toFile(output)
The deno script above is very simple, it takes in an input image path and output image path, then apply blur and resize and save to output path.
To run this sample directly with deno
:
deno run --allow-ffi \ --allow-env=npm_package_config_libvips,CWD \ --allow-read=/Users/user/Desktop/avatar.png src/deno-script.ts \ -i ~/Desktop/avatar.png -o ~/Desktop/avatar.jpeg
In the command above, we need to grant fine-grained permissions to Deno. It’s not a good idea to run Deno with all permissions.
Use Deno API
In the following sample code, we will use shell.createDenoCommand
to create a Deno command with permission settings.
See https://docs.api.kunkun.sh/interfaces/index.DenoRunConfig for options you can pass to shell.createDenoCommand
.
import { shell, path, TemplateUiCommand } from "@kksh/api/ui/template";
class ExtensionTemplate extends TemplateUiCommand { async load() { const denoCmd = shell.createDenoCommand( "$EXTENSION/src/deno-script.ts", ["-i=./avatar.png", "-o=./avatar-blur.jpeg"], { allowEnv: ["npm_package_config_libvips", "CWD"], allowRead: ["$DESKTOP"], allowAllFfi: true, cwd: await path.desktopDir(), } ); const denoRes = await denoCmd.execute(); console.log("stdout", denoRes.stdout); }}
The above TypeScript code sets CWD to the desktop directory, but you don’t have to. You can always just compute the absolute path for the input and output file.
$EXTENSION
was used to refer to the directory of the extension. It will be translated to the real path of your extension root at runtime.
$DESKTOP
is also a path alias that will be translated to the real path of your desktop at runtime.
You can find other path aliases in file system API.
Permissions in package.json
To ensure that an extension operates within a restricted scope, it must declare the necessary permissions in the package.json
file.
This step prevents the extension from executing any code beyond the predefined limits.
..."permissions": [ { "permission": "shell:deno:execute", "allow": [ { "path": "$EXTENSION/src/deno-script.ts", "env": ["npm_package_config_libvips", "CWD"], "ffi": "*", "read": ["$DESKTOP"] } ] }, ...]...
path
is the path to the deno script to execute.
The rest of the permissions are from Deno permissions.
net
env
read
write
run
ffi
sys
They are string[]
or "*"
, when set to "*"
, it allows all.
For example, setting "read": "*"
allows setting allowAllRead
to true in shell.createDenoCommand
, and will be translated to --allow-read
in the code execution.
Setting "read": ["$DESKTOP"]
allows you to set allowRead: ["$DESKTOP"]
in shell.createDenoCommand
, and will be translated to --allow-read=/Users/user/Desktop
in the code execution.