diff --git a/.mise.toml b/.mise.toml index 19b7a4f..3f6416f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -16,11 +16,15 @@ pipx = "latest" "pipx:cmake" = "latest" protoc = "latest" rclone = "latest" +ruff = "latest" rust = { version = "latest", components = "clippy" } typos = "latest" +uv = "latest" [tasks] editorconfig-check = "ec" +ruff-check = "ruff check services/ws-modules/" +ruff-fmt = "ruff format services/ws-modules/" [tasks.dprint-check] run = "dprint check" @@ -32,7 +36,7 @@ DPRINT_CACHE_DIR = "/tmp/dprint-cache" run = "dprint fmt" [tasks.fmt] -depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "taplo-fmt"] +depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "ruff-fmt", "taplo-fmt"] description = "Run repository formatters" [tasks.install-nightly] @@ -46,6 +50,7 @@ depends = [ "dprint-check", "editorconfig-check", "osv-scanner", + "ruff-check", "taplo-check", "typos", ] @@ -159,7 +164,15 @@ description = "Build the nfc workflow WASM module" dir = "services/ws-modules/nfc" run = "wasm-pack build . --target web" -[tasks.build-wasm] +[tasks.build-ws-pydata1-module] +description = "Build the pydata1 Python workflow module" +dir = "services/ws-modules/pydata1" +run = """ +uv build --wheel --out-dir pkg +cargo run -p pyproject-to-package-json +""" + +[tasks.build-modules] depends = [ "build-ws-audio1-module", "build-ws-bluetooth-module", @@ -170,6 +183,7 @@ depends = [ "build-ws-graphics-info-module", "build-ws-har1-module", "build-ws-nfc-module", + "build-ws-pydata1-module", "build-ws-sensor1-module", "build-ws-speech-recognition-module", "build-ws-video1-module", diff --git a/Cargo.toml b/Cargo.toml index 5f2094d..e34defa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2024" -license = "Apache-2.0 and MIT" +license = "Apache-2.0 or MIT" repository = "https://github.com/edge-toolkit/core" rust-version = "1.87.0" @@ -23,6 +23,7 @@ members = [ "services/ws-server", "services/ws-wasm-agent", "utilities/onnx", + "utilities/pyproject-to-package-json", ] resolver = "2" @@ -40,5 +41,6 @@ serde-env = "0.3" serde-inline-default = "1.0" serde_default = "0.2" serde_json = "1" +toml = "0.8" tracing = "0.1" uuid = { version = "1", features = ["v4", "v7"] } diff --git a/README.md b/README.md index 90b9e54..6bf59cf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ mise run ws-e2e-chrome ## Run ws agent in browser -### Build WASM and run the WS server +### Build modules and run the WS server In a separate terminal start OpenObserve (o2) and leave it running. @@ -31,7 +31,7 @@ Then start the fetch the ONNX models and run the server ```bash mise run download-models -mise run build-wasm +mise run build-modules mise run ws-server ``` @@ -39,8 +39,6 @@ Scan the QR-Code with a smart-phone camera and open the URL. Select the module to run in the drop-down, then click "Run module" button. -The module list is dynamically populated from the modules in [services/ws-modules](services/ws-modules). - Note: The WASM build disables WebAssembly reference types, so it can still load on older browsers such as Chrome 95. In a separate terminal, open the OpenObserve UX using: @@ -51,6 +49,17 @@ mise run open-o2 The server logs appear in the Logs section. +## Modules + +The module list is dynamically populated from the modules in [services/ws-modules](services/ws-modules). + +Each module must be a directory `pkg` containing a `package.json` that defines a `main` which contains a JavaScript file +that can load and run the module. + +Most of the module are built from Rust using `wasm-pack build --target web`. + +The module `pydata1` uses [pyodide](https://pyodide.org/) to run a Python script. + ## Grant This repository is part of a grant managed by the School of EECMS, Curtin University. diff --git a/services/ws-modules/audio1/src/lib.rs b/services/ws-modules/audio1/src/lib.rs index e25c220..4f94a6c 100644 --- a/services/ws-modules/audio1/src/lib.rs +++ b/services/ws-modules/audio1/src/lib.rs @@ -75,16 +75,6 @@ pub fn init() { info!("audio-capture module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { AUDIO_CAPTURE_RUNTIME.with(|runtime| runtime.borrow().is_some()) diff --git a/services/ws-modules/bluetooth/src/lib.rs b/services/ws-modules/bluetooth/src/lib.rs index f56df8c..38e06e3 100644 --- a/services/ws-modules/bluetooth/src/lib.rs +++ b/services/ws-modules/bluetooth/src/lib.rs @@ -96,16 +96,6 @@ pub fn init() { info!("bluetooth module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { false diff --git a/services/ws-modules/comm1/src/lib.rs b/services/ws-modules/comm1/src/lib.rs index 905d98a..773355e 100644 --- a/services/ws-modules/comm1/src/lib.rs +++ b/services/ws-modules/comm1/src/lib.rs @@ -18,16 +18,6 @@ pub fn init() { info!("comm1 workflow module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub async fn run() -> Result<(), JsValue> { log("comm1: entered run()")?; diff --git a/services/ws-modules/data1/src/lib.rs b/services/ws-modules/data1/src/lib.rs index c892c5f..c96086b 100644 --- a/services/ws-modules/data1/src/lib.rs +++ b/services/ws-modules/data1/src/lib.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use edge_toolkit::ws::WsMessage; use et_ws_wasm_agent::{WsClient, WsClientConfig, append_to_textarea}; use js_sys::{Promise, Reflect}; -use serde_json::json; use tracing::info; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -16,16 +15,6 @@ pub fn init() { info!("data1 workflow module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub async fn run() -> Result<(), JsValue> { let msg = "data1: entered run()"; diff --git a/services/ws-modules/face-detection/src/lib.rs b/services/ws-modules/face-detection/src/lib.rs index 863ed6d..cc4612b 100644 --- a/services/ws-modules/face-detection/src/lib.rs +++ b/services/ws-modules/face-detection/src/lib.rs @@ -118,16 +118,6 @@ pub fn init() { info!("face detection workflow module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { FACE_RUNTIME.with(|runtime| runtime.borrow().is_some()) diff --git a/services/ws-modules/geolocation/src/lib.rs b/services/ws-modules/geolocation/src/lib.rs index 8b114ca..0954eaa 100644 --- a/services/ws-modules/geolocation/src/lib.rs +++ b/services/ws-modules/geolocation/src/lib.rs @@ -106,16 +106,6 @@ pub fn init() { info!("geolocation module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { false diff --git a/services/ws-modules/graphics-info/src/lib.rs b/services/ws-modules/graphics-info/src/lib.rs index bbb71f2..0389834 100644 --- a/services/ws-modules/graphics-info/src/lib.rs +++ b/services/ws-modules/graphics-info/src/lib.rs @@ -328,16 +328,6 @@ pub fn init() { info!("graphics-info module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { false diff --git a/services/ws-modules/har1/src/lib.rs b/services/ws-modules/har1/src/lib.rs index edc8e7c..0342ef8 100644 --- a/services/ws-modules/har1/src/lib.rs +++ b/services/ws-modules/har1/src/lib.rs @@ -299,16 +299,6 @@ pub fn init() { info!("har1 workflow module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub async fn run() -> Result<(), JsValue> { set_har_status("har1: entered run()")?; diff --git a/services/ws-modules/nfc/src/lib.rs b/services/ws-modules/nfc/src/lib.rs index faeea4e..721d933 100644 --- a/services/ws-modules/nfc/src/lib.rs +++ b/services/ws-modules/nfc/src/lib.rs @@ -194,16 +194,6 @@ pub fn init() { info!("nfc module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { false diff --git a/services/ws-modules/pydata1/pkg/.gitignore b/services/ws-modules/pydata1/pkg/.gitignore new file mode 100644 index 0000000..d415af9 --- /dev/null +++ b/services/ws-modules/pydata1/pkg/.gitignore @@ -0,0 +1,2 @@ +*.whl +package.json diff --git a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js new file mode 100644 index 0000000..d7d2caf --- /dev/null +++ b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js @@ -0,0 +1,133 @@ +// et_ws_pydata1.js — Pyodide-based Python module shim +// Interface: default(wasmUrl), metadata(), run() + +const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/pyodide.js"; + +let pyodide = null; +let pyMod = null; + +function loadPyodideScript() { + return new Promise((resolve, reject) => { + if (globalThis.loadPyodide) return resolve(); + const s = document.createElement("script"); + s.src = PYODIDE_CDN; + s.onload = resolve; + s.onerror = reject; + document.head.appendChild(s); + }); +} + +export default async function init() { + await loadPyodideScript(); + pyodide = await globalThis.loadPyodide(); + + const pkgUrl = new URL("package.json", import.meta.url); + const pkg = await fetch(pkgUrl).then(r => r.json()); + const wheelName = `${pkg.name.replace(/-/g, "_")}-${pkg.version}-py3-none-any.whl`; + const wheelUrl = new URL(`${wheelName}`, import.meta.url); + + await pyodide.loadPackage("micropip"); + const micropip = pyodide.pyimport("micropip"); + await micropip.install(wheelUrl.href); + const pydata1 = pyodide.pyimport("pydata1"); + pyMod = { + run: pydata1.run, + }; +} + +export async function run() { + if (!pyMod) throw new Error("pydata1: not initialized"); + + const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${wsProtocol}//${window.location.host}/ws`; + + const { WsClient, WsClientConfig } = await import("/pkg/et_ws_wasm_agent.js"); + const client = new WsClient(new WsClientConfig(wsUrl)); + + let responseResolvers = []; + client.set_on_message((raw) => { + try { + const msg = JSON.parse(raw); + if (msg.type === "response") { + for (const { prefix, resolve } of responseResolvers) { + if (msg.message.startsWith(prefix)) { + responseResolvers = responseResolvers.filter(r => r.resolve !== resolve); + resolve(msg.message); + return; + } + } + } + } catch { /* ignore */ } + }); + + client.connect(); + + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + + for (let i = 0; i < 100; i++) { + if (client.get_state() === "connected") break; + await sleep(100); + if (i === 99) throw new Error("Timeout waiting for WebSocket connection"); + } + + let agentId = ""; + for (let i = 0; i < 100; i++) { + agentId = client.get_client_id(); + if (agentId) break; + await sleep(100); + if (i === 99) throw new Error("Timeout waiting for agent_id"); + } + + const wsSend = (msgStr) => { + // inject agent_id into fetch_file messages + const msg = JSON.parse(msgStr); + if (msg.type === "fetch_file") msg.agent_id = agentId; + client.send(JSON.stringify(msg)); + }; + + const waitForResponse = (prefix) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => { + responseResolvers = responseResolvers.filter(r => r.resolve !== resolve); + reject(new Error(`Timeout waiting for response with prefix: ${prefix}`)); + }, 5000); + responseResolvers.push({ + prefix, + resolve: (val) => { + clearTimeout(timer); + resolve(val); + }, + }); + }); + + const putFile = async (url, content) => { + const resp = await fetch(url, { method: "PUT", mode: "cors", body: content }); + if (!resp.ok) throw new Error(`PUT failed: ${resp.status}`); + }; + + const getFile = async (url) => { + const resp = await fetch(url); + if (!resp.ok) throw new Error(`GET failed: ${resp.status}`); + return resp.text(); + }; + + const log = (msg) => { + console.log(msg); + const el = document.getElementById("module-output"); + if (el) el.value = (el.value ? el.value + "\n" : "") + msg; + }; + + try { + await pyMod.run( + pyodide.toPy(wsSend), + pyodide.toPy(waitForResponse), + pyodide.toPy(putFile), + pyodide.toPy(getFile), + pyodide.toPy(sleep), + pyodide.toPy(log), + pyodide.toPy(() => {}), + ); + } finally { + client.disconnect(); + } +} diff --git a/services/ws-modules/pydata1/pkg/package.json b/services/ws-modules/pydata1/pkg/package.json new file mode 100644 index 0000000..6b4f1d0 --- /dev/null +++ b/services/ws-modules/pydata1/pkg/package.json @@ -0,0 +1,8 @@ +{ + "description": "Python data 1", + "license": "Apache-2.0 OR MIT", + "main": "et_ws_pydata1.js", + "name": "et-ws-pydata1", + "type": "module", + "version": "0.1.0" +} diff --git a/services/ws-modules/pydata1/pydata1/__init__.py b/services/ws-modules/pydata1/pydata1/__init__.py new file mode 100644 index 0000000..3c6c617 --- /dev/null +++ b/services/ws-modules/pydata1/pydata1/__init__.py @@ -0,0 +1,52 @@ +"""pydata1: Python implementation of the data1 workflow.""" + +import json +from datetime import datetime, timezone + + +async def run( + ws_send, wait_for_response, put_file, get_file, sleep_ms, log, set_status +) -> None: + """Execute the data1 workflow: connect, store, fetch, verify.""" + log("pydata1: entered run()") + + filename = "test_data.txt" + test_content = f"Hello from pydata1 at {datetime.now(timezone.utc).isoformat()}!" + + # 1. Request Store URL + log("pydata1: requesting store URL") + ws_send(json.dumps({"type": "store_file", "filename": filename})) + store_response = await wait_for_response("PUT to ") + store_url = store_response.replace("PUT to ", "") + + # 2. Perform PUT + msg = f"pydata1: storing data to {store_url}" + log(msg) + set_status(msg) + await put_file(store_url, test_content) + + # 3. Request Fetch URL + log("pydata1: requesting fetch URL") + ws_send(json.dumps({"type": "fetch_file", "filename": filename})) + fetch_response = await wait_for_response("GET from ") + fetch_url = fetch_response.replace("GET from ", "") + + # 4. Perform GET and Verify + msg = f"pydata1: fetching data from {fetch_url}" + log(msg) + set_status(msg) + retrieved = await get_file(fetch_url) + + if retrieved == test_content: + msg = "pydata1: VERIFICATION SUCCESS - data matches!" + log(msg) + set_status(msg) + else: + msg = f"pydata1: VERIFICATION FAILURE\nSent: {test_content}\nGot: {retrieved}" + log(msg) + set_status(msg) + raise RuntimeError("Data mismatch") + + await sleep_ms(2000) + log("pydata1: workflow complete") + set_status("pydata1: workflow complete") diff --git a/services/ws-modules/pydata1/pyproject.toml b/services/ws-modules/pydata1/pyproject.toml new file mode 100644 index 0000000..e65ec0b --- /dev/null +++ b/services/ws-modules/pydata1/pyproject.toml @@ -0,0 +1,18 @@ +[project] +dependencies = [] +description = "Python data 1" +license = "Apache-2.0 OR MIT" +name = "et-ws-pydata1" +requires-python = ">=3.10" +version = "0.1.0" + +[build-system] +build-backend = "uv_build" +requires = ["uv_build>=0.10.2,<0.11.0"] + +[tool.uv.build-backend] +module-name = "pydata1" +module-root = "" + +[tool.ws-module] +js-main = "et_ws_pydata1.js" diff --git a/services/ws-modules/sensor1/src/lib.rs b/services/ws-modules/sensor1/src/lib.rs index 237f80a..0d872f3 100644 --- a/services/ws-modules/sensor1/src/lib.rs +++ b/services/ws-modules/sensor1/src/lib.rs @@ -3,7 +3,6 @@ use std::rc::Rc; use et_web::{SENSOR_PERMISSION_GRANTED, request_sensor_permission}; use et_ws_wasm_agent::{js_bool_field, js_nested_object, js_number_field, set_textarea_value}; -use serde_json::json; use tracing::info; use wasm_bindgen::prelude::*; use web_sys::Event; @@ -296,16 +295,6 @@ pub fn init() { info!("sensor stream workflow module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { SENSOR_STREAM_RUNTIME.with(|runtime| runtime.borrow().is_some()) diff --git a/services/ws-modules/speech-recognition/src/lib.rs b/services/ws-modules/speech-recognition/src/lib.rs index 1dbf046..d526c81 100644 --- a/services/ws-modules/speech-recognition/src/lib.rs +++ b/services/ws-modules/speech-recognition/src/lib.rs @@ -284,16 +284,6 @@ pub fn init() { info!("speech-recognition module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { SPEECH_RECOGNITION_RUNTIME.with(|runtime| runtime.borrow().is_some()) diff --git a/services/ws-modules/video1/src/lib.rs b/services/ws-modules/video1/src/lib.rs index 7cc8c06..35293cd 100644 --- a/services/ws-modules/video1/src/lib.rs +++ b/services/ws-modules/video1/src/lib.rs @@ -75,16 +75,6 @@ pub fn init() { info!("video-capture module initialized"); } -#[wasm_bindgen] -pub fn metadata() -> JsValue { - serde_wasm_bindgen::to_value(&json!({ - "name": env!("CARGO_PKG_NAME"), - "description": env!("CARGO_PKG_DESCRIPTION"), - "version": env!("CARGO_PKG_VERSION"), - })) - .unwrap_or(JsValue::NULL) -} - #[wasm_bindgen] pub fn is_running() -> bool { VIDEO_CAPTURE_RUNTIME.with(|runtime| runtime.borrow().is_some()) diff --git a/services/ws-server/static/app.js b/services/ws-server/static/app.js index 32e5ea7..a7f83ac 100644 --- a/services/ws-server/static/app.js +++ b/services/ws-server/static/app.js @@ -35,32 +35,24 @@ const populateModuleDropdown = async () => { for (const name of moduleNames) { try { - const moduleKey = name; - const moduleUrl = `/modules/${name}/pkg/et_ws_${name.replace(/-/g, "_")}.js`; - const wasmUrl = `/modules/${name}/pkg/et_ws_${name.replace(/-/g, "_")}_bg.wasm`; - - append(`Loading metadata for ${name}...`); - const loadedModule = await import(`${moduleUrl}?v=${Date.now()}`); - await loadedModule.default(wasmUrl); - - let metadata = { name, description: "", version: "" }; - if (typeof loadedModule.metadata === "function") { - metadata = loadedModule.metadata(); + const pkgResp = await fetch(`/modules/${name}/pkg/package.json`); + if (!pkgResp.ok) { + append(`Skipping ${name}: no package.json (${pkgResp.status})`); + continue; } + const pkg = await pkgResp.json(); + + const moduleUrl = `/modules/${name}/pkg/${pkg.main}`; - WORKFLOW_MODULES.set(moduleKey, { - label: metadata.description || metadata.name || name, - moduleUrl, - wasmUrl, - loaded: loadedModule, - }); + const label = pkg.description || pkg.name || name; + WORKFLOW_MODULES.set(name, { label, moduleUrl, loaded: null }); const option = document.createElement("option"); - option.value = moduleKey; - option.textContent = WORKFLOW_MODULES.get(moduleKey).label; + option.value = name; + option.textContent = label; moduleSelect.appendChild(option); - append(`Successfully discovered module: ${name} (${metadata.version})`); + append(`Discovered module: ${name} (${pkg.version})`); } catch (error) { append(`Error discovering module ${name}: ${describeError(error)}`); console.error(`discovery error for ${name}:`, error); @@ -103,11 +95,9 @@ const loadWorkflowModule = async (moduleKey) => { const cacheBust = Date.now(); const moduleUrl = `${moduleConfig.moduleUrl}?v=${cacheBust}`; - const wasmUrl = `${moduleConfig.wasmUrl}?v=${cacheBust}`; append(`${moduleConfig.label} module: importing ${moduleUrl}`); const loadedModule = await import(moduleUrl); - append(`${moduleConfig.label} module: initializing ${wasmUrl}`); - await loadedModule.default(wasmUrl); + await loadedModule.default(); moduleConfig.loaded = loadedModule; return loadedModule; }; diff --git a/utilities/pyproject-to-package-json/Cargo.toml b/utilities/pyproject-to-package-json/Cargo.toml new file mode 100644 index 0000000..dc02c11 --- /dev/null +++ b/utilities/pyproject-to-package-json/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pyproject-to-package-json" +description = "Generate pkg/package.json from a Python ws-module's pyproject.toml" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "pyproject-to-package-json" +path = "src/main.rs" + +[dependencies] +serde.workspace = true +serde_json.workspace = true +toml.workspace = true diff --git a/utilities/pyproject-to-package-json/src/main.rs b/utilities/pyproject-to-package-json/src/main.rs new file mode 100644 index 0000000..b093bc2 --- /dev/null +++ b/utilities/pyproject-to-package-json/src/main.rs @@ -0,0 +1,57 @@ +use std::fs; +use std::path::PathBuf; + +use serde::Deserialize; +use serde_json::{Value, json}; + +#[derive(Deserialize)] +struct Project { + name: String, + version: String, + description: Option, + license: Option, +} + +#[derive(Deserialize)] +struct WsModule { + #[serde(rename = "js-main")] + js_main: String, +} + +#[derive(Deserialize)] +struct Tool { + #[serde(rename = "ws-module")] + ws_module: WsModule, +} + +#[derive(Deserialize)] +struct Pyproject { + project: Project, + tool: Tool, +} + +fn main() { + let pyproject_path = PathBuf::from("pyproject.toml"); + let src = fs::read_to_string(&pyproject_path) + .unwrap_or_else(|e| panic!("Failed to read {}: {e}", pyproject_path.display())); + + let pyproject: Pyproject = toml::from_str(&src).unwrap_or_else(|e| panic!("Failed to parse pyproject.toml: {e}")); + + let p = &pyproject.project; + let pkg: Value = json!({ + "name": p.name, + "type": "module", + "description": p.description.as_deref().unwrap_or(""), + "version": p.version, + "license": p.license.as_deref().unwrap_or(""), + "main": pyproject.tool.ws_module.js_main, + }); + + let out_path = PathBuf::from("pkg/package.json"); + fs::create_dir_all(out_path.parent().unwrap()).unwrap(); + let mut out = serde_json::to_string_pretty(&pkg).unwrap(); + out.push('\n'); + fs::write(&out_path, &out).unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_path.display())); + + println!("Wrote {}", out_path.display()); +}