Add TypeScript WASM guest runtime and examples.

This commit is contained in:
Sebastian Jeltsch
2025-09-09 15:54:36 +02:00
parent f1ec808622
commit 1fbf0f862e
107 changed files with 8970 additions and 12 deletions

3
examples/wasm-guest-js/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dist/
node_modules/
traildepot/

View File

@@ -0,0 +1,22 @@
TRAILBIN ?= RUST_BACKTRACE=1 cargo run --
TRAILDEPOT := traildepot
APPDIR := .
DISTDIR := ${APPDIR}/dist
ADDRESS := 127.0.0.1:4000
run: guest
${TRAILBIN} --data-dir=${TRAILDEPOT} run --address=${ADDRESS}
guest: ${TRAILDEPOT}/wasm/component.wasm
${TRAILDEPOT}/wasm/component.wasm: ${DISTDIR}/component.wasm
mkdir -p ${TRAILDEPOT}/wasm && cp $< $@
${DISTDIR}/component.wasm: src/*.js
pnpm i && pnpm build
clean:
rm -rf dist traildepot
.PHONY: guest run clean

View File

@@ -0,0 +1,20 @@
import pluginJs from "@eslint/js";
import globals from "globals";
export default [
pluginJs.configs.recommended,
{
ignores: ["dist/", "node_modules/"],
},
{
files: ["src/**/*.{js,mjs,cjs,mts,jsx}"],
rules: {
"no-var": "off",
},
languageOptions: {
globals: {
...globals.browser,
}
}
},
];

View File

@@ -0,0 +1,24 @@
{
"name": "example-wasm-guest-js",
"private": true,
"version": "0.0.0",
"scripts": {
"build:wasm": "jco componentize dist/index.mjs -w ../../guests/typescript/wit -o dist/component.wasm",
"build:wasm:aot": "jco componentize dist/index.mjs -w ../../guests/typescript/wit --aot -o dist/component.wasm",
"build": "vite build && npm run build:wasm",
"check": "eslint",
"format": "prettier -w src"
},
"dependencies": {
"trailbase-wasm": "workspace:*"
},
"devDependencies": {
"@bytecodealliance/componentize-js": "^0.18.4",
"@bytecodealliance/jco": "^1.13.3",
"@eslint/js": "^9.33.0",
"eslint": "^9.33.0",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"vite": "^7.1.2"
}
}

View File

@@ -0,0 +1,3 @@
import e from "./index";
export const { initEndpoint, incomingHandler } = e;

View File

@@ -0,0 +1,71 @@
import { defineConfig, addPeriodicCallback } from "trailbase-wasm";
import { HttpHandler, buildJsonResponse } from "trailbase-wasm/http";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";
export default defineConfig({
httpHandlers: [
HttpHandler.get("/fibonacci", (req) => {
const n = req.getQueryParam("n");
return fibonacci(n ? parseInt(n) : 40).toString();
}),
HttpHandler.get("/json", jsonHandler),
HttpHandler.post("/json", jsonHandler),
HttpHandler.get("/a", (req) => {
const n = req.getQueryParam("n");
return "a".repeat(n ? parseInt(n) : 5000);
}),
HttpHandler.get("/interval", () => {
let i = 0;
addPeriodicCallback(250, (cancel) => {
console.log(`callback #${i}`);
i += 1;
if (i >= 10) {
cancel();
}
});
}),
HttpHandler.get("/sleep", async (req) => {
const param = req.getQueryParam("ms");
const ms = param ? parseInt(param) : 500;
await sleep(ms);
return `slept: ${ms}ms`;
}),
HttpHandler.get("/count/{table}/", async (req) => {
const table = req.getPathParam("table");
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, []);
return `Got ${rows[0][0]} rows\n`;
}
}),
],
jobHandlers: [JobHandler.minutely("myjob", () => console.log("Hello Job!"))],
});
function jsonHandler(req) {
return buildJsonResponse(
req.json() ?? {
int: 5,
real: 4.2,
msg: "foo",
obj: {
nested: true,
},
},
);
}
function fibonacci(num) {
switch (num) {
case 0:
return 0;
case 1:
return 1;
default:
return fibonacci(num - 1) + fibonacci(num - 2);
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "./dist",
minify: false,
lib: {
entry: "./src/component.js",
fileName: "index",
formats: ["es"],
},
rollupOptions: {
external: (source) => source.startsWith("wasi:") || source.startsWith("trailbase:"),
},
},
})

3
examples/wasm-guest-ts/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dist/
node_modules/
traildepot/

View File

@@ -0,0 +1,22 @@
TRAILBIN ?= RUST_BACKTRACE=1 cargo run --
TRAILDEPOT := traildepot
APPDIR := .
DISTDIR := ${APPDIR}/dist
ADDRESS := 127.0.0.1:4000
run: guest
${TRAILBIN} --data-dir=${TRAILDEPOT} run --address=${ADDRESS}
guest: ${TRAILDEPOT}/wasm/component.wasm
${TRAILDEPOT}/wasm/component.wasm: ${DISTDIR}/component.wasm
mkdir -p ${TRAILDEPOT}/wasm && cp $< $@
${DISTDIR}/component.wasm: src/*.ts
pnpm i && pnpm build
clean:
rm -rf dist traildepot
.PHONY: guest run clean

View File

@@ -0,0 +1,30 @@
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/", "node_modules/"],
},
{
files: ["src/**/*.{js,mjs,cjs,mts,ts,tsx,jsx}"],
rules: {
// https://typescript-eslint.io/rules/no-explicit-any/
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-wrapper-object-types": "warn",
"@typescript-eslint/no-namespace": "off",
"no-var": "off",
// http://eslint.org/docs/rules/no-unused-vars
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
},
];

View File

@@ -0,0 +1,30 @@
{
"name": "example-wasm-guest-ts",
"private": true,
"version": "0.0.0",
"main": "dist/index.js",
"scripts": {
"generate:types": "jco types ../../guests/typescript/wit/ -o generated/types",
"build:tsc": "vite build",
"build:wasm": "jco componentize dist/index.mjs -w ../../guests/typescript/wit -o dist/component.wasm",
"build:wasm:aot": "jco componentize dist/index.mjs -w ../../guests/typescript/wit --aot -o dist/component.wasm",
"build": "npm run build:tsc && npm run build:wasm",
"check": "tsc --noEmit --skipLibCheck && eslint",
"format": "prettier -w src"
},
"dependencies": {
"trailbase-wasm": "workspace:*"
},
"devDependencies": {
"@bytecodealliance/componentize-js": "^0.18.4",
"@bytecodealliance/jco": "^1.13.3",
"@eslint/js": "^9.33.0",
"@types/node": "^24.3.0",
"eslint": "^9.33.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vitest": "^3.2.4"
}
}

View File

@@ -0,0 +1,3 @@
import e from "./index";
export const { initEndpoint, incomingHandler } = e;

View File

@@ -0,0 +1,71 @@
import { defineConfig, addPeriodicCallback } from "trailbase-wasm";
import { HttpHandler, Request, buildJsonResponse } from "trailbase-wasm/http";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";
export default defineConfig({
httpHandlers: [
HttpHandler.get("/fibonacci", (req: Request) => {
const n = req.getQueryParam("n");
return fibonacci(n ? parseInt(n) : 40).toString();
}),
HttpHandler.get("/json", jsonHandler),
HttpHandler.post("/json", jsonHandler),
HttpHandler.get("/a", (req: Request) => {
const n = req.getQueryParam("n");
return "a".repeat(n ? parseInt(n) : 5000);
}),
HttpHandler.get("/interval", () => {
let i = 0;
addPeriodicCallback(250, (cancel) => {
console.log(`callback #${i}`);
i += 1;
if (i >= 10) {
cancel();
}
});
}),
HttpHandler.get("/sleep", async (req: Request) => {
const param = req.getQueryParam("ms");
const ms: number = param ? parseInt(param) : 500;
await sleep(ms);
return `slept: ${ms}ms`;
}),
HttpHandler.get("/count/{table}/", async (req: Request) => {
const table = req.getPathParam("table");
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, []);
return `Got ${rows[0][0]} rows\n`;
}
}),
],
jobHandlers: [JobHandler.minutely("myjob", () => console.log("Hello Job!"))],
});
function jsonHandler(req: Request) {
return buildJsonResponse(
req.json() ?? {
int: 5,
real: 4.2,
msg: "foo",
obj: {
nested: true,
},
},
);
}
function fibonacci(num: number): number {
switch (num) {
case 0:
return 0;
case 1:
return 1;
default:
return fibonacci(num - 1) + fibonacci(num - 2);
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "es2022", /* Specify what module code is generated. */
"moduleResolution": "bundler",
"paths": {},
"outDir": "./dist/", /* Specify an output folder for all emitted files. */
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "./dist",
minify: false,
lib: {
entry: "./src/component.ts",
name: "runtime",
fileName: "index",
formats: ["es"],
},
rollupOptions: {
external: (source) => source.startsWith("wasi:") || source.startsWith("trailbase:"),
},
},
})