From 6d348cc21f941a49ad1e549b73c09833e162df8a Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 17 Jun 2026 05:46:09 +0800 Subject: [PATCH 001/119] Add RustPython generated WASM WS module --- .dockerignore | 1 + .github/workflows/test.yaml | 10 + .gitignore | 1 + .mise/config.toml | 193 ++++++++++++++++++ .mise/config.windows.toml | 9 - CLAUDE.md | 25 +++ README.md | 23 ++- config/conftest/policy/mise.rego | 2 +- .../mise-cargo-backend-allowlist.schema.json | 2 +- services/ws-modules/pywasm1/index.js | 7 + services/ws-modules/pywasm1/main.py | 1 + services/ws-web-runner/tests/modules.rs | 23 ++- 12 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 services/ws-modules/pywasm1/index.js create mode 100644 services/ws-modules/pywasm1/main.py diff --git a/.dockerignore b/.dockerignore index dd97eb3..97a1453 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ **/target/ **/.DS_Store services/ws-wasm-agent/pkg/ +services/ws-modules/pywasm1/pkg/ services/ws-server/static/models/ **/.zig-cache/ **/zig-out/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a09c89..130f5f0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -137,5 +137,15 @@ jobs: - name: Build WASM modules run: mise run build-modules + # Separate step (not in the default build-modules glob) because the + # rustpython compile is ~5 min on its own. Without these two steps, + # the et-ws-pywasm1 case in `services/ws-web-runner/tests/modules.rs` + # short-circuits with a "skipping {module}: pkg/ not built" log line. + - name: Clone RustPython for pywasm1 + run: git clone --depth=1 https://github.com/RustPython/RustPython.git "$HOME/rust/RustPython" + + - name: Build pywasm1 module + run: mise run build-pywasm1-module + - name: Run tests run: mise run test diff --git a/.gitignore b/.gitignore index 9284041..c94bd3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ .DS_Store services/ws-wasm-agent/pkg/ +services/ws-modules/pywasm1/pkg/ services/ws-server/static/models/ .zig-cache/ zig-out/ diff --git a/.mise/config.toml b/.mise/config.toml index 786fd7e..b04e767 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -36,6 +36,16 @@ action-validator = "latest" ast-grep = "latest" cargo-binstall = "latest" "cargo:cargo-expand" = "latest" +# RustPython -- a Rust reimplementation of CPython, used in mise task scripts +# where a small embeddable Python is enough (the heavier mise-managed +# `python` tool stays alongside for PyO3 linkage + workloads that need +# real CPython). cargo-binstall pulls prebuilts from quickinstall on +# macOS x64/arm64, Windows MSVC, and Linux x64 (gnu falls through to +# musl via binstall's built-in fallback when the gnu prebuilt is absent; +# rustpython only publishes musl). Linux aarch64 and Windows gnullvm +# have no prebuilt and fall back to source build. Allowlisted in +# config/conftest/policy/mise.rego's `allowed_cargo_no_prebuilt`. +"cargo:rustpython" = "0.5.0" taplo = "latest" watchexec = "latest" # vfox-chromedriver's PostInstall hook fails on Windows ("syntax of the @@ -807,6 +817,189 @@ description = "Run the Rust tests" depends = ["test:*"] description = "Run all tests: test:rust + any loaded guest test:" +# Probe how far rustpython gets at hosting real Python tooling: load an +# older extracted pip onto sys.path, use pipx to install + run cowsay, +# and fall back to a pip-only path. Tracks RustPython issues #5332 (pip +# on linux/macos) and #7956 (upstream-pending audit-hook fix). Five +# rustpython-side quirks the task works around: +# 1. The quickinstall prebuilt cargo-binstall fetches isn't built with +# `freeze-stdlib`, so its `Lib/` tree is missing -- without +# RUSTPYTHONPATH pointing at a matching `Lib/`, rustpython can't even +# import `encodings`. The task expects an upstream RustPython +# checkout at ~/rust/RustPython (RUSTPYTHON_REPO overrides). +# 2. RustPython's bundled ensurepip wheel is pip 26.1.1, whose +# `_prevent_further_imports` calls `sys.addaudithook` (CPython 3.8+) +# which rustpython 0.5.0 doesn't implement (upstream fix tracked in +# #7956). pip 26.0.1 is the last release before that hook was added +# (introduced in pip's upstream commit fb01b1830 on 2026-04-14, +# shipped in 26.1), so the task fetches that wheel. +# 3. RustPython's path-importer doesn't auto-treat `.whl` entries on +# sys.path as zipimports -- the wheel imports correctly via an +# explicit `zipimporter()` call but not via `import pip`. Unzip the +# wheel into a directory and put that on RUSTPYTHONPATH instead. +# 4. pipx's default backend is uv, which spawns rustpython as a fresh +# subprocess to probe interpreter info; the probe can't even +# `import site`/`encodings` because uv strips/replaces env. Set +# PIPX_DEFAULT_BACKEND=pip so pipx uses `python -m venv` directly -- +# env vars survive that path. +# 5. With the pip backend, pipx creates its shared-libs venv with +# `python -m venv --clear $root` (no `--without-pip`). RustPython's +# venv module then runs `ensurepip --upgrade --default-pip`, which +# extracts the bundled pip-26.1.1 wheel -- same audit-hook crash as +# (2). Every pipx release from 1.0.0 to 1.14.0 invokes venv the same +# way (verified by `git -C pipx show :src/pipx/shared_libs.py`), +# so no pipx version dodges this. Workaround: pre-create the shared +# venv at PIPX_SHARED_LIBS with `--without-pip`, drop pip 26.0.1's +# site-packages tree into it, and add a `bin/pip` shim -- pipx's +# `is_valid()` then short-circuits past `create()`. +# Each `step` prints its own exit code rather than aborting, so partial- +# failure runs show where rustpython diverges from CPython. All transient +# state lives under target/scratch/ so the task is self-contained and +# rerunnable. +[tasks.rustpython-investigation] +description = "Probe rustpython + pip 26.0.1 + pipx + cowsay (RustPython#5332/#7956)" +run = """ +scratch="{{ config_root }}/target/scratch/rustpython-investigation" +rp_dir="${RUSTPYTHON_REPO:-$HOME/rust/RustPython}" +if [ ! -d "$rp_dir/Lib/encodings" ]; then + echo "rustpython-investigation: no RustPython checkout at $rp_dir" >&2 + echo "clone: git clone https://github.com/RustPython/RustPython.git $rp_dir" >&2 + exit 1 +fi +coreutils mkdir -p "$scratch/site" "$scratch/pipx-home" "$scratch/pipx-bin" +pip_pkg="de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af" +pip_whl="$scratch/pip-26.0.1-py3-none-any.whl" +pip_extracted="$scratch/pip-extracted" +shared_venv="$scratch/shared" +if [ ! -f "$pip_whl" ]; then + echo "== fetch pip 26.0.1 wheel from PyPI ==" + pypi_args="--http-url https://files.pythonhosted.org --progress --ignore-existing" + pip_src=":http:packages/$pip_pkg/pip-26.0.1-py3-none-any.whl" + {{ vars.retry }} rclone copyto "$pip_src" "$pip_whl" $pypi_args +fi +if [ ! -d "$pip_extracted/pip" ]; then + echo "== unzip pip 26.0.1 wheel ==" + unzip -q -o "$pip_whl" -d "$pip_extracted" +fi +export RUSTPYTHONPATH="$pip_extracted:$scratch/site:$rp_dir/Lib" +export PIPX_HOME="$scratch/pipx-home" +export PIPX_BIN_DIR="$scratch/pipx-bin" +export PIPX_SHARED_LIBS="$shared_venv" +export PIPX_DEFAULT_BACKEND=pip + +if [ ! -f "$shared_venv/bin/pip" ]; then + echo "== pre-create pipx shared venv (--without-pip) + drop in pip 26.0.1 ==" + coreutils rm -rf "$shared_venv" + rustpython -m venv --without-pip "$shared_venv" + shared_site="$(coreutils ls -d "$shared_venv"/lib/*/site-packages)" + coreutils cp -R "$pip_extracted/pip" "$shared_site/" + coreutils cp -R "$pip_extracted"/pip-*.dist-info "$shared_site/" + coreutils cat > "$shared_venv/bin/pip" <<'PIP_SHIM' +#!/bin/sh +exec "$(dirname "$0")/python" -m pip "$@" +PIP_SHIM + coreutils chmod +x "$shared_venv/bin/pip" +fi + +# `if "$@"; then ...; else echo "-- exit=$?"; fi` keeps set -e from aborting +# the task when a probed step fails -- partial-failure runs are the whole point. +step() { + echo + echo "== $* ==" + if "$@"; then + echo "-- exit=0" + else + echo "-- exit=$?" + fi +} + +step rustpython --version +step rustpython -c "import pip; print(pip.__version__, pip.__file__)" +step rustpython -m pip --version +step rustpython -m pip install --target "$scratch/site" pipx +step rustpython -m pipx install cowsay +step rustpython -m pip install --target "$scratch/site" cowsay + +echo +echo "== run cowsay via pipx bin ==" +if [ -x "$PIPX_BIN_DIR/cowsay" ]; then + if "$PIPX_BIN_DIR/cowsay" -t "rustpython lives"; then + echo "-- exit=0" + else + echo "-- exit=$?" + fi +else + echo "-- cowsay not in $PIPX_BIN_DIR (pipx step likely failed)" +fi + +echo +echo "== run cowsay via rustpython -m cowsay ==" +step rustpython -m cowsay -t "rustpython lives via pip-only path" +""" +shell = "bash -euo pipefail -c" + +# Build rustpython_wasm out of the local RustPython checkout. One-time-ish: +# wasm-pack compiles rustpython + freeze-stdlib for wasm32, which takes +# ~5-10 min on first run; rerunning is fast (cargo incremental + the early +# `pkg/` exists check skips wasm-pack entirely if the output is present). +# Sourced from the same RUSTPYTHON_REPO env var rustpython-investigation +# uses. Deliberately outside the `build-ws-*` glob so the heavy build +# doesn't sneak into `mise run build-modules`. +[tasks.build-rustpython-wasm] +description = "One-time: build rustpython_wasm via wasm-pack against $RUSTPYTHON_REPO" +run = """ +rp_dir="${RUSTPYTHON_REPO:-$HOME/rust/RustPython}" +if [ ! -d "$rp_dir/crates/wasm" ]; then + echo "build-rustpython-wasm: no RustPython checkout at $rp_dir/crates/wasm" >&2 + echo "clone: git clone https://github.com/RustPython/RustPython.git $rp_dir" >&2 + exit 1 +fi +if [ -f "$rp_dir/crates/wasm/pkg/rustpython_wasm_bg.wasm" ]; then + echo "build-rustpython-wasm: pkg/ already present at $rp_dir/crates/wasm/pkg" + echo "rm -rf that dir to force a rebuild" + exit 0 +fi +wasm-pack build "$rp_dir/crates/wasm" --target web {{ vars.no_opt }} +""" +shell = "bash -euo pipefail -c" + +# Build the pywasm1 ws-module. Copies the rustpython_wasm artifacts produced +# by build-rustpython-wasm into pkg/, alongside this module's main.py and +# index.js shim, and writes a pkg/package.json so et-modules-service picks +# the dir up. Task name intentionally omits the `ws-` prefix so the default +# `build-modules` glob (`build-ws-*`) doesn't drag this in -- the rustpython +# build is too slow to belong in a default build. +[tasks.build-pywasm1-module] +depends = ["build-rustpython-wasm"] +description = "Build the pywasm1 module (rustpython_wasm + main.py)" +dir = "services/ws-modules/pywasm1" +run = """ +rp_pkg="${RUSTPYTHON_REPO:-$HOME/rust/RustPython}/crates/wasm/pkg" +coreutils rm -rf pkg +coreutils mkdir -p pkg +coreutils cp "$rp_pkg"/rustpython_wasm.js "$rp_pkg"/rustpython_wasm.d.ts pkg/ +coreutils cp "$rp_pkg"/rustpython_wasm_bg.wasm "$rp_pkg"/rustpython_wasm_bg.wasm.d.ts pkg/ +coreutils cp -R "$rp_pkg/snippets" pkg/ +coreutils cp index.js main.py pkg/ +coreutils cat > pkg/package.json <<'PKG_JSON' +{ + "name": "et-ws-pywasm1", + "type": "module", + "version": "0.1.0", + "main": "index.js", + "files": [ + "index.js", + "main.py", + "rustpython_wasm.js", + "rustpython_wasm.d.ts", + "rustpython_wasm_bg.wasm", + "rustpython_wasm_bg.wasm.d.ts" + ] +} +PKG_JSON +""" +shell = "bash -euo pipefail -c" + # --- Cross-language orchestration (stand-ins for the MISE_ENV=all mise lacks). # `$ALL_LANGS` is auto-discovered in `[env]` above from the config..toml # filenames, so adding a language needs no edits here. diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index f50886f..c7b1792 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -112,15 +112,6 @@ PYO3_PYTHON = "{{ vars.py3_win }}" # .dylib that ship in the release. The workspace `ort` dep keeps `copy-dylibs` # so the DLL lands next to linked binaries. ORT_PREFER_DYNAMIC_LINK = "1" -# Point cargo-binstall at the MSVC triple. The host is gnullvm (see -# CARGO_BUILD_TARGET above), and the cargo:* tools we install (cargo-expand) -# don't publish gnullvm prebuilts -- so binstall falls back to `cargo install`, -# which then tries to link build scripts via llvm-mingw clang/lld and dies on -# LNK1143 (lld + rustc CGU COMDAT mismatch). Steering binstall to the MSVC -# triple makes it fetch the upstream MSVC release binary, which runs fine on -# this host: target only governs what the tool links against, not what -# executes it. -BINSTALL_TARGETS = "x86_64-pc-windows-msvc" # Prepend conda:m2-gnupg's nested binary dir to PATH. mise's per-tool # .mise-bins resolves `/bin/X` and `/Library/bin/X` cleanly, # but msys2-format packages put binaries one level deeper at diff --git a/CLAUDE.md b/CLAUDE.md index 5da66e7..460585f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -365,6 +365,31 @@ you add a path to `.gitignore`, update their ignore lists too: A new linter that surfaces ignored paths in its output belongs on this list. +## Do NOT add gitignored paths to ec / dprint / typos config + +`editorconfig-checker` (`ec`), `dprint`, and `typos` already honor `.gitignore` +on their own. If a path is in `.gitignore`, those three linters skip it — full +stop. Do **not** add a redundant exclude to `.editorconfig`'s `[path/**]` +blocks, `config/dprint.jsonc`'s `excludes`, or `config/typos.toml`'s +`extend-exclude` "just to be safe" or "to be explicit". A redundant exclude is +a lie — it implies the path needs special handling when in fact `.gitignore` +already covers it, and the next reader has to grep two places to understand +the same fact. + +The right move when one of these linters flags a generated / vendored / build- +output tree: + +1. Add the path to `.gitignore` (and run `mise run gen:dockerignore` if it + also belongs in `.dockerignore`). +2. Stop. Do not touch `.editorconfig` / `dprint.jsonc` / `typos.toml`. + +The narrow exception: a path that **must** stay tracked in git (so cannot go +in `.gitignore`) but still needs the linter to ignore it. `generated/` trees +that we commit (`generated/python-rest/`, `generated/python-ws/`, etc.) are +the canonical case — see the existing `[generated/python-rest/**]` block in +`.editorconfig` for the shape. Reach for a config-file exclude only after +confirming gitignoring isn't viable. + ## No `scripts/` directory Do not create a `scripts/` directory or drop loose shell/Python scripts in the diff --git a/README.md b/README.md index af60011..2ce315a 100644 --- a/README.md +++ b/README.md @@ -224,15 +224,30 @@ that can load and run the module. Under each module in `ws-modules`, the package can be found in a subdirectory `pkg`. -Most of the module are built from Rust using `wasm-pack build --target web`. +Modules target one of three runners. -There are also modules written in: +### Browser runner ([ws-web-runner](services/ws-web-runner)) + +Modules loaded by a web browser, or natively under Deno by `et-ws-web-runner`. +Most are Rust built with `wasm-pack build --target web`; other languages: - Dart - Java - .Net C# -- Python, using [pyodide](https://pyodide.org/) -- Zig, including C code. +- Python, using [pyodide](https://pyodide.org/) and [RustPython](https://rustpython.github.io/) +- Zig, including C code + +### WASI runner ([ws-wasi-runner](services/ws-wasi-runner)) + +Modules built as WASI Preview 2 components and run under wasmtime: + +- Rust +- Python, via [componentize-py](https://github.com/bytecodealliance/componentize-py) + +### PyO3 runner ([ws-pyo3-runner](services/ws-pyo3-runner)) + +Native CPython modules linked via [PyO3](https://pyo3.rs) -- used for +workloads that need a real CPython runtime (e.g. PyTorch inference). ## Root module diff --git a/config/conftest/policy/mise.rego b/config/conftest/policy/mise.rego index 32def00..0a1c4fa 100644 --- a/config/conftest/policy/mise.rego +++ b/config/conftest/policy/mise.rego @@ -44,7 +44,7 @@ deny contains msg if { # slow surprise on the critical path. second_tier_platform := {"linux/arm64", "macos/x64"} -allowed_cargo_no_prebuilt := {"cargo:cargo-expand", "cargo:dart-typegen"} +allowed_cargo_no_prebuilt := {"cargo:cargo-expand", "cargo:dart-typegen", "cargo:rustpython"} cargo_scoped_to_second_tier(spec) if { is_object(spec) diff --git a/config/taplo/mise-cargo-backend-allowlist.schema.json b/config/taplo/mise-cargo-backend-allowlist.schema.json index 31232b0..94d1c9e 100644 --- a/config/taplo/mise-cargo-backend-allowlist.schema.json +++ b/config/taplo/mise-cargo-backend-allowlist.schema.json @@ -9,7 +9,7 @@ "propertyNames": { "anyOf": [ { "not": { "pattern": "^cargo:" } }, - { "enum": ["cargo:cargo-expand", "cargo:dart-typegen", "cargo:findutils", "cargo:ryl"] } + { "enum": ["cargo:cargo-expand", "cargo:dart-typegen", "cargo:findutils", "cargo:rustpython", "cargo:ryl"] } ] } } diff --git a/services/ws-modules/pywasm1/index.js b/services/ws-modules/pywasm1/index.js new file mode 100644 index 0000000..a5e7a55 --- /dev/null +++ b/services/ws-modules/pywasm1/index.js @@ -0,0 +1,7 @@ +import init, { pyExec } from "./rustpython_wasm.js"; + +export async function run() { + const src = await fetch(new URL("./main.py", import.meta.url)).then((r) => r.text()); + await init(); + pyExec(src, { stdout: (msg) => console.log(msg) }); +} diff --git a/services/ws-modules/pywasm1/main.py b/services/ws-modules/pywasm1/main.py new file mode 100644 index 0000000..c7bf3d7 --- /dev/null +++ b/services/ws-modules/pywasm1/main.py @@ -0,0 +1 @@ +print("hello from pywasm1 (rustpython compiled to WASM)") diff --git a/services/ws-web-runner/tests/modules.rs b/services/ws-web-runner/tests/modules.rs index c1b7507..5e02440 100644 --- a/services/ws-web-runner/tests/modules.rs +++ b/services/ws-web-runner/tests/modules.rs @@ -37,7 +37,8 @@ #![expect( clippy::expect_used, clippy::panic, - reason = "test code: process spawn failure or non-zero exit fails the test" + clippy::print_stdout, + reason = "test code: process spawn failure or non-zero exit fails the test; pywasm1 skip uses println" )] use rstest::rstest; @@ -49,11 +50,31 @@ use rstest::rstest; #[case::dotnet_data1("et-ws-dotnet-data1")] #[case::java_data1("et-ws-java-data1")] #[case::zig_data1("et-ws-zig-data1")] +#[case::pywasm1("et-ws-pywasm1")] fn module_runs_successfully(#[case] module: &str) { + if module == "et-ws-pywasm1" && !pywasm1_pkg_built() { + println!("skipping {module}: pkg/ not built (run `mise run build-pywasm1-module`)"); + return; + } let server = et_ws_test_server::start(); run_runner_with_timeout(module, &server.ws_url, 90); } +/// pywasm1 depends on building `rustpython_wasm` from an external clone +/// (`build-rustpython-wasm`), which the default `build-modules` task skips +/// because of the multi-minute rustpython compile. Skip the test on hosts +/// without the pkg/ output instead of failing -- pinning to it would block +/// every CI lane and dev box that hasn't paid that cost. +#[expect( + clippy::single_call_fn, + reason = "distinct probe step; kept named for the skip-trace log line" +)] +fn pywasm1_pkg_built() -> bool { + edge_toolkit::config::get_project_root() + .join("services/ws-modules/pywasm1/pkg/package.json") + .exists() +} + /// Spawn two runners against one ws-server and assert both finish ok. /// Used by communication modules that need to discover at least one peer /// via `et-list-agents` before they can complete (comm1, dart-comm1). From 503f522f666d7b9e598c587529f07126fd7863fe Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 17 Jun 2026 02:44:27 +0800 Subject: [PATCH 002/119] Force driver --- Dockerfile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Dockerfile b/Dockerfile index 095ab08..69eb7c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -192,6 +192,22 @@ RUN mise run build-modules && rm -rf target/ # doesn't initialize yet, so prefer a DRI device for now. FROM precompile AS test ENV NVIDIA_VISIBLE_DEVICES=all NVIDIA_DRIVER_CAPABILITIES=all +# Force the Vulkan loader to consider lavapipe (CPU software renderer) only. +# Without it, distros whose `mesa-vulkan-drivers` package also ships a +# PowerVR ICD (notably Azure Linux base/core, where `powervr_mesa_icd.json` +# is staged alongside `lvp_icd.x86_64.json`) try the PVR ICD first; on a +# GPU-less CI runner that ICD fails to enumerate DRM devices, blocking +# adapter discovery before lavapipe gets reached, and the wasi-graphics-info +# test crashes verbatim as: +# wgpu_hal::vulkan::instance: GENERAL [pvr_device.c (0x0)] +# Failed to enumerate drm devices (errno 2: No such file or directory) +# (VK_ERROR_INITIALIZATION_FAILED) +# ... +# Error: ...WebgpuError(... message='no GPU adapter available') +# When the docker host DOES pass `--device /dev/dri`, lvp is still a valid +# CPU fallback alongside the real GPU's ICD; the `lvp_icd` pattern selects +# the software path either way. +ENV VK_LOADER_DRIVERS_SELECT=lvp_icd # Vulkan runtime + Mesa drivers via whichever package manager the base has. # Debian/Ubuntu: libvulkan1 + mesa-vulkan-drivers; Fedora and Azure Linux: # vulkan-loader + mesa-vulkan-drivers (same Mesa name, different loader name). From 67d5617be7f7daf4eaa484f7e859cae98b806a27 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 17 Jun 2026 06:00:53 +0800 Subject: [PATCH 003/119] another win fix --- .mise/config.dotnet.toml | 18 ++++++++++-------- services/ws-modules/pywasm1/main.py | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.mise/config.dotnet.toml b/.mise/config.dotnet.toml index 93dec23..0fd6139 100644 --- a/.mise/config.dotnet.toml +++ b/.mise/config.dotnet.toml @@ -46,15 +46,17 @@ if [ "${OS:-}" = "Windows_NT" ]; then echo "[emcc-find] emcc_bat=${emcc_bat:-}" if [ -n "$emcc_bat" ]; then emcc_dir="$(dirname "$emcc_bat")" - # `cygpath -w` (Git Bash) converts /c/Users/... to C:\Users\... so the - # prepended PATH entry resolves in cmd.exe when MSBuild's shells - # out to bare `emcc`. Without conversion, Git Bash hands Windows - # processes a PATH where this dir may not be recognised in the form - # cmd's resolver expects. + # Normalise to MSYS Unix form (/c/Users/...). Bash uses `:` as the PATH + # separator; MSYS rewrites the whole PATH (`:`->`;`, /c/Users/... -> + # C:\Users\...) when it spawns a Win32 child like dotnet.exe, but only + # when each entry is in Unix form. A `C:\Users\...` entry escapes the + # conversion and the trailing `:` glues it to the next entry, so cmd.exe + # (run by MSBuild's for `emcc @...rsp`) sees no emscripten dir + # and reports `'emcc' is not recognized` (exit 9009). if command -v cygpath >/dev/null 2>&1; then - emcc_dir_win="$(cygpath -w "$emcc_dir")" - echo "[emcc-find] cygpath conversion: $emcc_dir -> $emcc_dir_win" - emcc_dir="$emcc_dir_win" + emcc_dir_unix="$(cygpath -u "$emcc_dir")" + echo "[emcc-find] cygpath conversion: $emcc_dir -> $emcc_dir_unix" + emcc_dir="$emcc_dir_unix" fi export PATH="$emcc_dir:$PATH" echo "[emcc-find] prepended to PATH: $emcc_dir" diff --git a/services/ws-modules/pywasm1/main.py b/services/ws-modules/pywasm1/main.py index c7bf3d7..62fa330 100644 --- a/services/ws-modules/pywasm1/main.py +++ b/services/ws-modules/pywasm1/main.py @@ -1 +1,3 @@ +"""pywasm1 smoke module: print a line under rustpython compiled to WASM.""" + print("hello from pywasm1 (rustpython compiled to WASM)") From 81e8fb2b6605b6cfc5024051679f560b2cd601b0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 17 Jun 2026 07:37:43 +0800 Subject: [PATCH 004/119] oxlint & oxfmt, and fixes --- .github/workflows/test.yaml | 30 +++++-- .mise/config.toml | 82 ++++++++++++++++++- .mise/config.windows.toml | 15 ++++ CLAUDE.md | 57 +++++++++++++ Dockerfile | 13 ++- Dockerfile.nanoserver | 11 ++- config/dprint.jsonc | 36 ++++---- config/oxfmtrc.jsonc | 24 ++++++ config/oxlintrc.jsonc | 45 ++++++++++ config/semgrep/prefer-yaml-toml.yaml | 12 ++- .../dart-comm1/pkg/et_ws_dart_comm1.js | 2 +- .../dotnet-data1/pkg/et_ws_dotnet_data1.js | 10 ++- .../java-data1/pkg/et_ws_java_data1.js | 10 ++- .../ws-modules/pydata1/pkg/et_ws_pydata1.js | 34 ++++---- .../ws-modules/pyface1/pkg/et_ws_pyface1.js | 17 ++-- .../zig-data1/pkg/et_ws_zig_data1.js | 21 +++-- .../zig-data1/pkg/et_ws_zig_data1_worker.js | 6 +- services/ws-server/static/app.js | 18 ++-- services/ws-web-runner/src/shim.js | 22 +++-- 19 files changed, 363 insertions(+), 102 deletions(-) create mode 100644 config/oxfmtrc.jsonc create mode 100644 config/oxlintrc.jsonc diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 130f5f0..d8aca5e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,17 +67,16 @@ jobs: ) ) }} env: - # Windows-only: skip the two cargo: source-builds that need a fully-set-up - # gnullvm toolchain at install time. The Nano docker (Dockerfile.nanoserver) - # sets the same flag; on the windows-latest runner here the gnullvm - # rust-std isn't reliably in place when cargo install runs them, leading to - # the rustc diagnostic: + # Windows-only: skip the cargo: source-build that would otherwise be + # triggered when the gnullvm rust-std isn't fully laid down -- rustc fails + # with: # error[E0463]: can't find crate for `core` # = note: the `x86_64-pc-windows-gnullvm` target may not be installed - # Both are dev-only tools (cargo-expand: macro debugging; dart-typegen: - # used only by `mise run gen:dart-ws`, which the dart config skips on - # Windows anyway); neither is in the test suite's hot path. - MISE_DISABLE_TOOLS: ${{ startsWith(matrix.os, 'windows') && 'cargo:cargo-expand,cargo:dart-typegen' || '' }} + # cargo:dart-typegen is dev-only (used only by `mise run gen:dart-ws`, + # which the dart config skips on Windows) and has no quickinstall release, + # so it can't be steered to a prebuilt the way cargo:cargo-expand and + # cargo:rustpython can (see config.windows.toml's install_env overrides). + MISE_DISABLE_TOOLS: ${{ startsWith(matrix.os, 'windows') && 'cargo:dart-typegen' || '' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -147,5 +146,18 @@ jobs: - name: Build pywasm1 module run: mise run build-pywasm1-module + # `build-pywasm1-module` already copied the wasm-pack output into + # services/ws-modules/pywasm1/pkg/, so the RustPython source tree + its + # ~3 GB cargo target/ is dead weight once we get here. Removing it + # reclaims the disk headroom the test-ws-web-runner link phase needs + # (it otherwise hit `No space left on device (os error 28)` on + # ubuntu-latest's 14 GB runner). Free both the per-language tier + # subdir and the wasm-pack output dir. + - name: Free disk space after pywasm1 build + if: runner.os == 'Linux' + run: | + rm -rf "$HOME/rust/RustPython" + df -h / + - name: Run tests run: mise run test diff --git a/.mise/config.toml b/.mise/config.toml index b04e767..9183749 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -42,8 +42,13 @@ cargo-binstall = "latest" # real CPython). cargo-binstall pulls prebuilts from quickinstall on # macOS x64/arm64, Windows MSVC, and Linux x64 (gnu falls through to # musl via binstall's built-in fallback when the gnu prebuilt is absent; -# rustpython only publishes musl). Linux aarch64 and Windows gnullvm -# have no prebuilt and fall back to source build. Allowlisted in +# rustpython only publishes musl). Linux aarch64 has no prebuilt and +# falls back to source build. Windows would too -- the host's rustup +# default-host is gnullvm (see config.windows.toml), and quickinstall +# only publishes msvc -- but the Windows-specific install_env override +# below sets CARGO_BUILD_TARGET to msvc so cargo-binstall fetches the +# msvc release directly (target governs link, not exec; msvc binary runs +# fine on gnullvm host). Allowlisted in # config/conftest/policy/mise.rego's `allowed_cargo_no_prebuilt`. "cargo:rustpython" = "0.5.0" taplo = "latest" @@ -153,6 +158,49 @@ profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2" version = "nightly" +# oxlint + oxfmt -- Rust-native JS/TS linter + formatter from the oxc-project, +# pulled directly from the shared `apps_v` GitHub release. Aqua's +# registry entry trips mise's date-filter on the `apps_v` tag prefix +# (https://github.com/oxc-project/oxc/discussions/14452), and `cargo:` is +# out because neither binary is published as a crates.io crate. The http: +# backend with per-platform `url` + `rename_exe` strips the target-suffix +# baked into the upstream binary names (e.g. `oxlint-x86_64-apple-darwin` +# -> `oxlint`) so the bare command is on PATH the same way every other +# mise tool installs. +[tools."http:oxlint"] +bin = "oxlint" +rename_exe = "oxlint" +version = "1.69.0" +[tools."http:oxlint".platforms.linux-arm64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v{{version}}/oxlint-aarch64-unknown-linux-gnu.tar.gz" +[tools."http:oxlint".platforms.linux-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v{{version}}/oxlint-x86_64-unknown-linux-gnu.tar.gz" +[tools."http:oxlint".platforms.macos-arm64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v{{version}}/oxlint-aarch64-apple-darwin.tar.gz" +[tools."http:oxlint".platforms.macos-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v{{version}}/oxlint-x86_64-apple-darwin.tar.gz" +[tools."http:oxlint".platforms.windows-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v{{version}}/oxlint-x86_64-pc-windows-msvc.zip" + +# oxfmt's binary version lags oxlint's (they ship from the same apps_v tag +# but track separate semver). Pin the URL to the release tag that bundles +# both (`apps_v` -- here apps_v1.69.0); bump that string when +# bumping oxlint above. +[tools."http:oxfmt"] +bin = "oxfmt" +rename_exe = "oxfmt" +version = "0.54.0" +[tools."http:oxfmt".platforms.linux-arm64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v1.69.0/oxfmt-aarch64-unknown-linux-gnu.tar.gz" +[tools."http:oxfmt".platforms.linux-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v1.69.0/oxfmt-x86_64-unknown-linux-gnu.tar.gz" +[tools."http:oxfmt".platforms.macos-arm64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v1.69.0/oxfmt-aarch64-apple-darwin.tar.gz" +[tools."http:oxfmt".platforms.macos-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v1.69.0/oxfmt-x86_64-apple-darwin.tar.gz" +[tools."http:oxfmt".platforms.windows-x64] +url = "https://github.com/oxc-project/oxc/releases/download/apps_v1.69.0/oxfmt-x86_64-pc-windows-msvc.zip" + [vars] # clang-tidy's resource-dir arg (points clang at its builtin headers, e.g. # stddef.h). Empty default; config.linux.toml sets it from conda:clangxx. @@ -234,9 +282,26 @@ DPRINT_CACHE_DIR = "{{ config_root }}/target/dprint-cache" [tasks.dprint-fmt] run = "dprint fmt" +[tasks.oxfmt-check] +description = "Check JS/TS formatting (oxfmt)" +# oxfmt walks the working tree from CWD, honouring .gitignore (so target/, +# all pkg/ dirs, node_modules, etc. are skipped). Runs alongside dprint -- +# dprint's typescript plugin formats the same files, and config/oxfmtrc.jsonc +# tunes oxfmt (printWidth=120) so its output matches dprint's typescript +# plugin (which is matched-back via arrowFunction.useParentheses + member +# linePerExpression in config/dprint.jsonc). `fmt:rust` orders oxfmt-fmt +# before dprint-fmt so dprint has the last word on residual disagreements. +run = "oxfmt --config config/oxfmtrc.jsonc --check" + +[tasks.oxfmt-fmt] +description = "Format JS/TS sources in place (oxfmt)" +run = "oxfmt --config config/oxfmtrc.jsonc --write" + [tasks."fmt:rust"] # Rust + universal formatters. Always-loaded, so `fmt`'s glob always matches it. -depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "taplo-fmt"] +# oxfmt-fmt before dprint-fmt: both touch JS/TS, so the second pass wins; +# dprint stays the source of truth for any disagreements on JS style. +depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "oxfmt-fmt", "taplo-fmt"] description = "Run the Rust + universal repo-wide formatters" [tasks.fmt] @@ -266,6 +331,8 @@ depends = [ "hadolint-check", "link-check", "ls-lint-check", + "oxfmt-check", + "oxlint-check", "ryl-check", "semgrep-check", "taplo-check", @@ -299,6 +366,15 @@ run = "zizmor --offline --no-progress -c config/zizmor.yaml .github/workflows" description = "Lint file and directory naming conventions (ls-lint)" run = "ls-lint --config config/ls-lint.yaml" +[tasks.oxlint-check] +description = "Lint JavaScript/TypeScript sources (oxlint)" +# oxlint walks the working tree from CWD, honouring .gitignore; no path arg +# needed. Errors fail the task (correctness category, lifted in oxlintrc); warnings +# print but don't fail -- the initial introduction kept stylistic warnings visible +# rather than blocking on the existing module shims' style. Tighten with +# `--deny-warnings` once those are cleaned up. +run = "oxlint --config config/oxlintrc.jsonc" + [tasks.ast-grep-check] # --no-ignore hidden so the gha-* YAML rules reach .github/workflows (ast-grep # skips dot-dirs by default); gitignored paths like target/ stay skipped. diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index c7b1792..c87f0a8 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -21,6 +21,21 @@ "conda:m2-gnupg" = "2.2.41.1" "github:mstorsjo/llvm-mingw" = { version = "20260602", matching = "ucrt-x86_64" } +# Windows overrides of cargo: entries in config.toml. cargo-binstall reads +# CARGO_BUILD_TARGET via its clap env binding (binstall/crates/bin/src/args.rs: +# 88-95). The [env] block in this file sets it to gnullvm so the repo's own +# Rust builds link gnullvm-only -- but neither crate publishes a gnullvm +# quickinstall release, so without this override binstall finds nothing and +# falls through to source (which then fails on missing gnullvm rust-std). +# `install_env` is a per-tool override applied only during the install step +# (mise's tool_version_options.rs: CoreToolOptions.install_env) and routes +# binstall to the msvc release. The resulting binary runs fine on a gnullvm +# host -- target only affects what the tool links, not what executes it. +# (cargo:dart-typegen has no quickinstall release at all -- not just no +# gnullvm one -- so it stays in MISE_DISABLE_TOOLS for the Windows test lane.) +"cargo:cargo-expand" = { version = "latest", install_env = { CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc" } } +"cargo:rustpython" = { version = "0.5.0", install_env = { CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc" } } + # busybox-w32 `sh` (POSIX ash) -- the shell the bash-tasks run on. A native Win32 # single exe with no cygwin/msys runtime, so it loads on a bare Nano Server, # unlike conda's msys2 bash whose msys-2.0.dll imports KERNEL32!IdnToAscii / diff --git a/CLAUDE.md b/CLAUDE.md index 460585f..25ef580 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -352,6 +352,63 @@ ast-grep has no TOML grammar, so it **cannot** lint TOML — use a taplo schema a semgrep `generic` rule there. If none of the above can express a check, propose adding a new mise-installable linter rather than scripting it by hand. +## Writing JS/TS that both dprint and oxfmt accept + +Both formatters run on every `*.js` / `*.ts` file. `config/dprint.jsonc`'s +`typescript` block and `config/oxfmtrc.jsonc` are tuned to agree on the +structural choices each tool exposes as config knobs (arrow-paren style, +binary-operator position, member-chain breaking, line width). Two +unconfigurable structural decisions still trip them up — both reduce to: + +> **When an assignment statement exceeds `printWidth` (120), dprint and oxfmt +> break it in different places.** + +dprint inserts a linebreak after `=`, keeping the RHS one connected unit. +oxfmt prefers to keep the assignment one-line and break inside the RHS, OR +in template-literal cases, refuses to break the literal at all and overflows +silently. Either way they disagree, and there's no dprint/oxfmt knob to +reconcile them. + +The fix is in the source, not the config: **keep every assignment statement +under 120 chars**. When the RHS is genuinely long, extract intermediates. + +The `` lines below stop dprint reformatting the BAD +examples (which would otherwise hide the bug we're showing). + + +```js +// Bad: long template literal assignment -- dprint breaks after `=`, oxfmt +// keeps inline, neither check is happy after the other has run. +logEl.textContent = `Initializing WASM from ${someLongUrl}\nWebSocket endpoint: ${wsUrl}`; +``` + +```js +// Good: extract the long sub-expression first. +const wasmUrl = "/modules/et-ws-wasm-agent/et_ws_wasm_agent_bg.wasm"; +logEl.textContent = `Initializing WASM from ${wasmUrl}\nWebSocket endpoint: ${wsUrl}`; +``` + + +```js +// Bad: nested ternary / `||` chain straddles the line limit. +const wsUrl = globalThis.__ET_WS_URL || + `${(typeof location !== "undefined" ? location.protocol : "ws:") === "https:" ? "wss:" : "ws:"}//${ + typeof location !== "undefined" ? location.host : "localhost:8080" + }/ws`; +``` + +```js +// Good: lift the parts to named locals; each line stays well under 120. +const loc = typeof location !== "undefined" ? location : null; +const wsProto = loc?.protocol === "https:" ? "wss:" : "ws:"; +const wsHost = loc?.host ?? "localhost:8080"; +const wsUrl = globalThis.__ET_WS_URL || `${wsProto}//${wsHost}/ws`; +``` + +If you really cannot get the line under 120 (rare in practice), the next +escape is `// dprint-ignore` on that single line — but verify oxfmt also +leaves it alone after dprint runs. + ## Linter ignores: keep in sync with .gitignore Some linters walk the working tree directly and never read `.gitignore`. When diff --git a/Dockerfile b/Dockerfile index 69eb7c4..8cc1881 100644 --- a/Dockerfile +++ b/Dockerfile @@ -204,10 +204,15 @@ ENV NVIDIA_VISIBLE_DEVICES=all NVIDIA_DRIVER_CAPABILITIES=all # (VK_ERROR_INITIALIZATION_FAILED) # ... # Error: ...WebgpuError(... message='no GPU adapter available') -# When the docker host DOES pass `--device /dev/dri`, lvp is still a valid -# CPU fallback alongside the real GPU's ICD; the `lvp_icd` pattern selects -# the software path either way. -ENV VK_LOADER_DRIVERS_SELECT=lvp_icd +# The value is a comma-separated list of fnmatch globs matched against the +# ICD JSON filename (no implicit substring match -- a bare `lvp_icd` rejected +# every driver on ubuntu:24.04 with "Driver 'lvp_icd.json' ignored because not +# selected by env var 'VK_LOADER_DRIVERS_SELECT'"). The glob below catches +# both `lvp_icd.json` (Debian/Ubuntu/Fedora) and `lvp_icd.x86_64.json` +# (Azure Linux). When the docker host DOES pass `--device /dev/dri`, lvp is +# still a valid CPU fallback alongside the real GPU's ICD; the lvp glob +# selects the software path either way. +ENV VK_LOADER_DRIVERS_SELECT=lvp_icd* # Vulkan runtime + Mesa drivers via whichever package manager the base has. # Debian/Ubuntu: libvulkan1 + mesa-vulkan-drivers; Fedora and Azure Linux: # vulkan-loader + mesa-vulkan-drivers (same Mesa name, different loader name). diff --git a/Dockerfile.nanoserver b/Dockerfile.nanoserver index 2b0dc92..ec81043 100644 --- a/Dockerfile.nanoserver +++ b/Dockerfile.nanoserver @@ -320,17 +320,20 @@ RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=`, not `x =>` + // - memberExpression.linePerExpression → break chained `.then().catch()` + // - binaryExpression.operatorPosition=sameLine → `&&` / `||` trailing + // the previous line, not leading the next "typescript": { + "arrowFunction.useParentheses": "force", + "binaryExpression.operatorPosition": "sameLine", + "memberExpression.linePerExpression": true, }, - "yaml": { - }, - "excludes": [ - "**/node_modules", - "**/*-lock.json", - "data/", - ], + "yaml": {}, + "excludes": ["**/node_modules", "**/*-lock.json", "data/"], "plugins": [ "https://github.com/speakeasy-api/dprint-plugin-java/releases/latest/download/dprint_plugin_java.wasm", "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm", diff --git a/config/oxfmtrc.jsonc b/config/oxfmtrc.jsonc new file mode 100644 index 0000000..e64cd57 --- /dev/null +++ b/config/oxfmtrc.jsonc @@ -0,0 +1,24 @@ +// oxfmt config (JSON/JSONC like oxlint -- same restriction). Knobs tuned so +// oxfmt's JS/TS output matches dprint's typescript plugin output on this +// repo, so the two formatters can coexist without fighting: +// +// - printWidth=120 matches dprint's lineWidth default (also our editorconfig +// and ruff line cap). +// - semi, singleQuote, trailingComma defaults already match dprint's +// prefer/double/onlyMultiLine equivalents. +// +// oxfmt does NOT expose an arrowParens equivalent (always `(x) =>`) -- where +// dprint produced bare `x =>` we now match oxfmt's parenthesised form once +// `oxfmt --write` runs. Drift in the other direction is configured in +// `dprint.jsonc`'s typescript block (arrowFunction.useParentheses=force, +// binaryExpression.operatorPosition=sameLine, memberExpression.linePerExpression). +// +// Exclude `**/*.toml` -- oxfmt's content-based detection misidentifies +// `[section]`-shaped TOML files (e.g. config/clippy.toml) as JS arrays and +// tries to reformat them. Extension-based filtering would skip these; lacking +// that, we ignore explicitly. +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json", + "printWidth": 120, + "ignorePatterns": ["**/*.toml"], +} diff --git a/config/oxlintrc.jsonc b/config/oxlintrc.jsonc new file mode 100644 index 0000000..ec389be --- /dev/null +++ b/config/oxlintrc.jsonc @@ -0,0 +1,45 @@ +// oxlint config. Has to be JSON/JSONC: oxlint's loader (`crates/oxc_linter/ +// src/config/oxlintrc.rs`'s `is_json_ext`) only accepts `.json` / `.jsonc` +// and outright rejects everything else with "Only JSON configuration files +// are supported" -- no YAML, no TOML, no TS despite docs hinting otherwise. +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "categories": { + "correctness": "error", + "suspicious": "warn", + "perf": "warn", + "restriction": "off", + "pedantic": "off", + "style": "off", + "nursery": "off", + }, + "rules": { + // __ET_* are sentinel globals shared across the deno shim and the + // ws-modules: et-ws-web-runner substitutes the `__ET_HTTP_BASE__` / + // `__ET_WS_URL__` placeholders before exec, and the resulting + // `globalThis.__ET_*` is how the modules read the host base URL. + // Intentional cross-boundary underscore naming, like webpack's + // `__webpack_*` or Python dunders. + "eslint/no-underscore-dangle": ["warn", { "allow": ["__ET_HTTP_BASE", "__ET_WS_URL"] }], + // Worker.postMessage takes no targetOrigin (unlike Window.postMessage); + // the rule can't distinguish, so it false-positives on every worker-side + // / main-side `worker.postMessage({...})` call we have. + "unicorn/require-post-message-target-origin": "off", + }, + "overrides": [ + { + // shim.js installs DOM stubs (Window / HTMLElement / HTMLCanvasElement / + // Image classes, `obj.onload = null` property inits) so Deno-hosted + // wasm-bindgen modules see a browser-shaped global. The style rules + // below conflict with that design: stub classes MUST exist as classes + // for `instanceof`, on-handler properties MUST exist as settable + // null-initialised fields. + "files": ["**/services/ws-web-runner/src/shim.js"], + "rules": { + "typescript/no-extraneous-class": "off", + "unicorn/prefer-add-event-listener": "off", + "unicorn/consistent-function-scoping": "off", + }, + }, + ], +} diff --git a/config/semgrep/prefer-yaml-toml.yaml b/config/semgrep/prefer-yaml-toml.yaml index a1b8407..c0dc81f 100644 --- a/config/semgrep/prefer-yaml-toml.yaml +++ b/config/semgrep/prefer-yaml-toml.yaml @@ -8,14 +8,20 @@ rules: - "*.jsonl" exclude: # Formats whose tooling mandates JSON/JSONC -- everything else should be - # YAML or TOML. Add a file here only if its tool can't read YAML/TOML. + # YAML or TOML. Add a file here only if its tool can't read YAML/TOML; + # if the tool also supports JSONC, prefer that extension over plain + # JSON so the config can carry inline comments. - "*.schema.json" # JSON Schema documents - "package.json" # npm manifests - "/.vscode/*.json" # editor config - "dprint.jsonc" # dprint config - ".dprint.jsonc" + - "oxlintrc.jsonc" # oxlint: only JSON / JSONC supported + - "oxfmtrc.jsonc" # oxfmt: only JSON / JSONC supported pattern-regex: \A message: >- - Prefer YAML or TOML over JSON/JSONC/JSONL for config. If a tool requires - this format, add the file to this rule's paths.exclude allowlist. + Prefer YAML or TOML over JSON/JSONC/JSONL for config. If the tool + requires a JSON-family format, prefer .jsonc over .json so the config + can carry inline comments, and add the file to this rule's + paths.exclude allowlist. severity: ERROR diff --git a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js index 44be2b9..9d32d5e 100644 --- a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js +++ b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js @@ -28,7 +28,7 @@ export async function run() { } catch (e) { console.error("dart-comm1 raw error:", e, "boxed:", e?.error); const msg = e?.error?.toString?.() ?? e?.message ?? String(e); - throw new Error(msg); + throw new Error(msg, { cause: e }); } finally { delete globalThis.WsClient; delete globalThis.WsClientConfig; diff --git a/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js b/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js index 4ace1f7..377ec7a 100644 --- a/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js +++ b/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js @@ -7,7 +7,9 @@ export default async function init() { const { dotnet } = await import(new URL("dotnet.js", import.meta.url).href); const { getAssemblyExports, setModuleImports } = await dotnet.create(); - let ws = null, wsState = "disconnected", agentId = ""; + let ws = null, + wsState = "disconnected", + agentId = ""; setModuleImports("dotnet-data1", { wsConnect: (url) => { @@ -34,11 +36,11 @@ export default async function init() { wsGetState: () => wsState, wsGetAgentId: () => agentId ?? "", putFile: (url, body) => - fetch(url, { method: "PUT", body }).then(r => { + fetch(url, { method: "PUT", body }).then((r) => { if (!r.ok) throw new Error(`PUT failed: ${r.status}`); }), getFile: (url) => - fetch(url).then(r => { + fetch(url).then((r) => { if (!r.ok) throw new Error(`GET failed: ${r.status}`); return r.text(); }), @@ -49,7 +51,7 @@ export default async function init() { setStatus: (msg) => appendOutput(msg), getWsUrl: () => `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/ws`, getIsoTimestamp: () => new Date().toISOString(), - sleep: (ms) => new Promise(r => setTimeout(r, ms)), + sleep: (ms) => new Promise((r) => setTimeout(r, ms)), }); exports = await getAssemblyExports("dotnet-data1"); diff --git a/services/ws-modules/java-data1/pkg/et_ws_java_data1.js b/services/ws-modules/java-data1/pkg/et_ws_java_data1.js index 40828f1..040cadd 100644 --- a/services/ws-modules/java-data1/pkg/et_ws_java_data1.js +++ b/services/ws-modules/java-data1/pkg/et_ws_java_data1.js @@ -4,7 +4,9 @@ let javaRun = null; export default async function init() { - let ws = null, wsState = "disconnected", agentId = ""; + let ws = null, + wsState = "disconnected", + agentId = ""; // TeaVM @JSBody calls reference `host` as a global globalThis.host = { @@ -32,11 +34,11 @@ export default async function init() { wsGetState: () => wsState, wsGetAgentId: () => agentId ?? "", putFile: (url, body) => - fetch(url, { method: "PUT", body }).then(r => { + fetch(url, { method: "PUT", body }).then((r) => { if (!r.ok) throw new Error(`PUT failed: ${r.status}`); }), getFile: (url) => - fetch(url).then(r => { + fetch(url).then((r) => { if (!r.ok) throw new Error(`GET failed: ${r.status}`); return r.text(); }), @@ -46,7 +48,7 @@ export default async function init() { }, setStatus: (msg) => appendOutput(msg), getWsUrl: () => `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/ws`, - sleep: (ms) => new Promise(r => setTimeout(r, ms)), + sleep: (ms) => new Promise((r) => setTimeout(r, ms)), }; const jsUrl = new URL("classes.js", import.meta.url).href; diff --git a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js index 0fa4e21..b5bb401 100644 --- a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js +++ b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js @@ -11,18 +11,20 @@ function loadPyodideScript() { if (globalThis.loadPyodide) return resolve(); // In Deno / non-browser environments, import() the module directly. if ( - typeof document === "undefined" || typeof document.createElement !== "function" - || !document.head || typeof document.head.appendChild !== "function" - || typeof Deno !== "undefined" + typeof document === "undefined" || + typeof document.createElement !== "function" || + !document.head || + typeof document.head.appendChild !== "function" || + typeof Deno !== "undefined" ) { - const baseUrl = (typeof globalThis.__ET_HTTP_BASE === "string") - ? globalThis.__ET_HTTP_BASE - : ""; + const baseUrl = typeof globalThis.__ET_HTTP_BASE === "string" ? globalThis.__ET_HTTP_BASE : ""; const url = baseUrl + PYODIDE_CDN; - import(url).then((mod) => { - if (mod.loadPyodide) globalThis.loadPyodide = mod.loadPyodide; - resolve(); - }).catch(reject); + import(url) + .then((mod) => { + if (mod.loadPyodide) globalThis.loadPyodide = mod.loadPyodide; + resolve(); + }) + .catch(reject); return; } const s = document.createElement("script"); @@ -60,11 +62,11 @@ export default async function init() { await installEtRestClient(pyodide); const injectWheel = async (wheelName) => { - const bytes = new Uint8Array(await fetch(new URL(wheelName, import.meta.url)).then(r => r.arrayBuffer())); + const bytes = new Uint8Array(await fetch(new URL(wheelName, import.meta.url)).then((r) => r.arrayBuffer())); pyodide.FS.writeFile(`/tmp/${wheelName}`, bytes); pyodide.runPython(`import sys\nsys.path.insert(0, "/tmp/${wheelName}")`); }; - const pkg = await fetch(new URL("package.json", import.meta.url)).then(r => r.json()); + const pkg = await fetch(new URL("package.json", import.meta.url)).then((r) => r.json()); const ownWheel = `${pkg.name.replace(/-/g, "_")}-${pkg.version}-py3-none-any.whl`; await injectWheel(ownWheel); @@ -77,10 +79,10 @@ export default async function init() { export async function run() { if (!pyMod) throw new Error("pydata1: not initialized"); - const wsUrl = globalThis.__ET_WS_URL - || `${ - (typeof location !== "undefined" ? location.protocol : "ws:") === "https:" ? "wss:" : "ws:" - }//${(typeof location !== "undefined" ? location.host : "localhost:8080")}/ws`; + const loc = typeof location !== "undefined" ? location : null; + const wsProto = loc?.protocol === "https:" ? "wss:" : "ws:"; + const wsHost = loc?.host ?? "localhost:8080"; + const wsUrl = globalThis.__ET_WS_URL || `${wsProto}//${wsHost}/ws`; const wasmAgent = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); await wasmAgent.default(); diff --git a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js index d9c0216..5d5acf4 100644 --- a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js +++ b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js @@ -124,7 +124,7 @@ async function infer(state) { const geometry = py.preprocess_geometry(video.videoWidth, video.videoHeight).toJs({ dict_converter: Object.fromEntries, }); - const canvas = workCanvas ??= document.createElement("canvas"); + const canvas = (workCanvas ??= document.createElement("canvas")); canvas.width = cfg.input_width; canvas.height = cfg.input_height; @@ -134,12 +134,7 @@ async function infer(state) { const tensor = imageDataToTensor(ctx.getImageData(0, 0, canvas.width, canvas.height).data); const outputs = await state.session.run({ - [state.inputName]: new globalThis.ort.Tensor("float32", tensor, [ - 1, - cfg.input_height, - cfg.input_width, - 3, - ]), + [state.inputName]: new globalThis.ort.Tensor("float32", tensor, [1, cfg.input_height, cfg.input_width, 3]), }); return pyodide.toPy({ @@ -197,15 +192,15 @@ function cleanup(state) { } function setStatus(message) { - const element = document.getElementById("module-output"); - if (element) element.value = message; + const output = document.getElementById("module-output"); + if (output) output.value = message; } function log(message) { const line = `[pyface1] ${message}`; console.log(line); - const element = document.getElementById("log"); - if (element) element.textContent = element.textContent ? `${element.textContent}\n${line}` : line; + const logEl = document.getElementById("log"); + if (logEl) logEl.textContent = logEl.textContent ? `${logEl.textContent}\n${line}` : line; } function sleep(ms) { diff --git a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js index 8aaba89..6b1a48b 100644 --- a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js +++ b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js @@ -47,7 +47,9 @@ export async function run() { }; return new Promise((resolve, reject) => { - let ws = null, wsState = "disconnected", agentId = ""; + let ws = null, + wsState = "disconnected", + agentId = ""; const poll = () => { if (Atomics.load(ctrl, 0) !== 1) { @@ -62,10 +64,13 @@ export async function run() { switch (type) { case 0: - setTimeout(() => { - respond(); - poll(); - }, parseInt(payload) || 0); + setTimeout( + () => { + respond(); + poll(); + }, + parseInt(payload) || 0, + ); return; case 1: ws = new WebSocket(payload); @@ -150,7 +155,11 @@ export async function run() { worker.onmessage = (e) => { if (e.data.done) { worker.terminate(); - e.data.ret === 0 ? resolve() : reject(new Error("zig-data1: run() returned " + e.data.ret)); + if (e.data.ret === 0) { + resolve(); + } else { + reject(new Error("zig-data1: run() returned " + e.data.ret)); + } } }; worker.onerror = (e) => { diff --git a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1_worker.js b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1_worker.js index 5a05be3..8bfde14 100644 --- a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1_worker.js +++ b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1_worker.js @@ -1,12 +1,14 @@ // et_ws_zig_data1_worker.js — Web Worker for zig-data1 WASM module const DATA_OFFSET = 16; let ctrl, data, wasmMemory; -const enc = new TextEncoder(), dec = new TextDecoder(); +const enc = new TextEncoder(), + dec = new TextDecoder(); const readStr = (ptr, len) => dec.decode(new Uint8Array(wasmMemory.buffer, ptr, len)); // String payload + string aux, response is a UTF-8 string (legacy ops). function call(type, payload = "", aux = "") { - const pb = enc.encode(payload), ab = enc.encode(aux); + const pb = enc.encode(payload), + ab = enc.encode(aux); data.set(pb); if (ab.length) data.set(ab, pb.length); Atomics.store(ctrl, 3, ab.length); diff --git a/services/ws-server/static/app.js b/services/ws-server/static/app.js index 6b665e9..e3549e8 100644 --- a/services/ws-server/static/app.js +++ b/services/ws-server/static/app.js @@ -23,9 +23,7 @@ const append = (line) => { logEl.textContent += `\n${line}`; }; -const describeError = (error) => ( - error instanceof Error ? error.message : String(error) -); +const describeError = (error) => (error instanceof Error ? error.message : String(error)); const WORKFLOW_MODULES = new Map(); @@ -132,9 +130,9 @@ const runSelectedWorkflowModule = async () => { const loadedModule = await loadWorkflowModule(moduleKey); if ( - typeof loadedModule.is_running === "function" - && loadedModule.is_running() - && typeof loadedModule.stop === "function" + typeof loadedModule.is_running === "function" && + loadedModule.is_running() && + typeof loadedModule.stop === "function" ) { append(`${moduleConfig.label} module: calling stop()`); loadedModule.stop(); @@ -178,8 +176,8 @@ const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; const retainedAgentId = readStoredAgentId(); -logEl.textContent = - `Initializing WASM from /modules/et-ws-wasm-agent/et_ws_wasm_agent_bg.wasm\nWebSocket endpoint: ${wsUrl}`; +const wasmUrl = "/modules/et-ws-wasm-agent/et_ws_wasm_agent_bg.wasm"; +logEl.textContent = `Initializing WASM from ${wasmUrl}\nWebSocket endpoint: ${wsUrl}`; updateAgentCard( retainedAgentId ? "Found retained agent ID in local storage. It will be re-used on connect." @@ -240,9 +238,7 @@ try { const selectedModule = WORKFLOW_MODULES.get(moduleSelect.value); runModuleButton.disabled = true; moduleSelect.disabled = true; - runModuleButton.textContent = selectedModule - ? `Running ${selectedModule.label}...` - : "Running module..."; + runModuleButton.textContent = selectedModule ? `Running ${selectedModule.label}...` : "Running module..."; try { await runSelectedWorkflowModule(); diff --git a/services/ws-web-runner/src/shim.js b/services/ws-web-runner/src/shim.js index 64219f2..ea694de 100644 --- a/services/ws-web-runner/src/shim.js +++ b/services/ws-web-runner/src/shim.js @@ -31,7 +31,9 @@ if (typeof globalThis.navigator === "object" && !globalThis.navigator.userAgent) value: "et-ws-web-runner/deno", configurable: true, }); - } catch (_) { /* navigator is read-only in some setups, ignore */ } + } catch { + /* navigator is read-only in some setups, ignore */ + } } // `document` stub -- enough for wasm-bindgen modules that probe DOM @@ -67,11 +69,13 @@ if (typeof globalThis.document === "undefined") { // A `