From 9d2240be678052381ca093894db3ed2a65ceff18 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:44:40 +0000 Subject: [PATCH 1/4] fix(ci): install tree-sitter from GitHub releases; accept SPDX on line 2 --ignore-scripts on tree-sitter-cli left the binary missing (ENOENT in migration-assistant). actions-lock prepends a managed-by comment, so the workflow linter now looks for SPDX in the first 5 lines. Also land the affinescript-vite compile-pass scaffold (Refs #56). --- .github/workflows/ci.yml | 28 ++++++------------ .github/workflows/workflow-linter.yml | 4 ++- affinescript-vite/README.adoc | 23 +++++++++++++++ affinescript-vite/index.js | 42 +++++++++++++++++++++++++++ affinescript-vite/package.json | 14 +++++++++ docs/ECOSYSTEM.adoc | 5 +++- scripts/install-tree-sitter-cli.sh | 38 ++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 affinescript-vite/README.adoc create mode 100644 affinescript-vite/index.js create mode 100644 affinescript-vite/package.json create mode 100755 scripts/install-tree-sitter-cli.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 578b151c..ead9b5ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,10 +77,9 @@ jobs: # `dune runtest` — not an optional extra that merely widens # coverage. Dropping it turns the walker suite red, which is # the intended behaviour: see that file's header comment. - # Pin exact version + --ignore-scripts: Sonar githubactions:S8543 - # (unlocked versions) and S6505 (lifecycle scripts). Same binary - # the grammar's package.json asks for (^0.25.0 floor). - run: npm install -g --ignore-scripts tree-sitter-cli@0.25.0 + # GitHub release binary (no npm postinstall). --ignore-scripts + # on tree-sitter-cli@0.25.0 left the `tree-sitter` binary missing. + run: ./scripts/install-tree-sitter-cli.sh - name: Build pinned tree-sitter-rescript grammar run: ./editors/tree-sitter-rescript/scripts/install.sh - name: Build @@ -241,10 +240,9 @@ jobs: # so this job needs the same grammar prerequisites as `build`. # Before the skip was removed, this job was green while running # zero walker tests. - # Pin exact version + --ignore-scripts: Sonar githubactions:S8543 - # (unlocked versions) and S6505 (lifecycle scripts). Same binary - # the grammar's package.json asks for (^0.25.0 floor). - run: npm install -g --ignore-scripts tree-sitter-cli@0.25.0 + # GitHub release binary (no npm postinstall). --ignore-scripts + # on tree-sitter-cli@0.25.0 left the `tree-sitter` binary missing. + run: ./scripts/install-tree-sitter-cli.sh - name: Build pinned tree-sitter-rescript grammar run: ./editors/tree-sitter-rescript/scripts/install.sh - name: Run tests with bisect_ppx instrumentation @@ -347,17 +345,9 @@ jobs: with: node-version: "20" - name: Install tree-sitter CLI - # npm install of tree-sitter-cli is the fast CI path (~5 s vs. - # ~5 min for `cargo install tree-sitter-cli`). The repo's - # preferred local path is cargo (see editors/tree-sitter-rescript/ - # README.md) — both produce the same `tree-sitter` binary that - # the install script invokes via `command -v`. The version - # tracks `tree-sitter-rescript`'s package.json devDependency - # range. - # Pin exact version + --ignore-scripts: Sonar githubactions:S8543 - # (unlocked versions) and S6505 (lifecycle scripts). Same binary - # the grammar's package.json asks for (^0.25.0 floor). - run: npm install -g --ignore-scripts tree-sitter-cli@0.25.0 + # GitHub release binary (no npm postinstall). --ignore-scripts + # on tree-sitter-cli@0.25.0 left the `tree-sitter` binary missing. + run: ./scripts/install-tree-sitter-cli.sh - name: Build pinned tree-sitter-rescript grammar # Direct script invocation rather than `just install-grammar` — # GitHub Actions runners do not ship `just` preinstalled, and diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index d06b225a..1422f8a9 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -28,7 +28,9 @@ jobs: errors=0 for f in .github/workflows/*.yml .github/workflows/*.yaml; do [ -f "$f" ] || continue - if ! head -1 "$f" | grep -q "SPDX-License-Identifier"; then + # actions-lock prepends a managed-by comment, so SPDX is + # often line 2. Accept it anywhere in the first 5 lines. + if ! head -5 "$f" | grep -q "SPDX-License-Identifier"; then echo "ERROR: $f missing SPDX header" errors=$((errors + 1)) fi diff --git a/affinescript-vite/README.adoc b/affinescript-vite/README.adoc new file mode 100644 index 00000000..13a63706 --- /dev/null +++ b/affinescript-vite/README.adoc @@ -0,0 +1,23 @@ += affinescript-vite +:toc: macro + +toc::[] + +Vite plugin **scaffold** for AffineScript (issue #56 remainder). +Not a production bundler integration. + +== Use + +[source,javascript] +---- +import affinescript from "affinescript-vite"; + +export default { + plugins: [affinescript()], +}; +---- + +`*.affine` modules are compiled with `affinescript compile --bun-esm`. +The compiler must be on `PATH` (or set `AFFINESCRIPT` / `compiler` option). + +Deno-ESM is retired; the emit target is Bun-ESM. diff --git a/affinescript-vite/index.js b/affinescript-vite/index.js new file mode 100644 index 00000000..72e4c2a1 --- /dev/null +++ b/affinescript-vite/index.js @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MPL-2.0 +/** + * Vite plugin scaffold for AffineScript (#56). + * + * Transforms `*.affine` sources by shelling out to `affinescript compile + * --bun-esm`. This is a compile-pass wiring, not a production bundler + * integration: the compiler must be on PATH, and the plugin does not + * ship a JS-hosted AffineScript frontend. + */ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +export default function affinescriptVite(options = {}) { + const compiler = options.compiler || process.env.AFFINESCRIPT || "affinescript"; + return { + name: "affinescript", + enforce: "pre", + async transform(_code, id) { + const filename = id.split("?")[0]; + if (!filename.endsWith(".affine")) return null; + const dir = mkdtempSync(join(tmpdir(), "affinescript-vite-")); + const src = join(dir, "input.affine"); + const out = join(dir, "out.bun.js"); + try { + writeFileSync(src, _code); + const r = spawnSync(compiler, ["compile", src, "-o", out, "--bun-esm"], { + encoding: "utf8", + }); + if (r.status !== 0) { + const msg = (r.stderr || r.stdout || "affinescript compile failed").trim(); + this.error(msg); + return null; + } + return { code: readFileSync(out, "utf8"), map: null }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + }; +} diff --git a/affinescript-vite/package.json b/affinescript-vite/package.json new file mode 100644 index 00000000..097db0e5 --- /dev/null +++ b/affinescript-vite/package.json @@ -0,0 +1,14 @@ +{ + "name": "affinescript-vite", + "version": "0.0.1", + "description": "Vite plugin scaffold: compile .affine files with --bun-esm", + "license": "MPL-2.0", + "type": "module", + "main": "index.js", + "exports": { + ".": "./index.js" + }, + "peerDependencies": { + "vite": ">=5" + } +} diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc index 2be9f4b4..df08e0b0 100644 --- a/docs/ECOSYSTEM.adoc +++ b/docs/ECOSYSTEM.adoc @@ -166,7 +166,10 @@ against a real Int-handle host DOM, mutation log asserted. `stdlib/Dom.affine` + `stdlib/Console.affine`. Remaining: full idaptik surface. -|`affinescript-vite` |scaffold |Build-tool integration shell. +|`affinescript-vite` |scaffold |In-tree Vite plugin shell +(`affinescript-vite/`): transforms `*.affine` via +`affinescript compile --bun-esm`. Compiler must be on PATH. Not a +production bundler. |`affinescript-deno-test` |historical |Smoke-test harness from the retired Deno-ESM target. JS-host ESM is Bun-ESM (`--bun-esm`). diff --git a/scripts/install-tree-sitter-cli.sh b/scripts/install-tree-sitter-cli.sh new file mode 100755 index 00000000..998d74dc --- /dev/null +++ b/scripts/install-tree-sitter-cli.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Install a pinned tree-sitter CLI binary from GitHub Releases. +# +# Used by CI instead of `npm install -g --ignore-scripts tree-sitter-cli`: +# that package's binary is fetched in a postinstall script, so +# --ignore-scripts (Sonar S6505) leaves `tree-sitter` missing (ENOENT). +# A release tarball has no lifecycle scripts. +set -euo pipefail + +VER="${TREE_SITTER_CLI_VERSION:-0.25.0}" +DEST="${TREE_SITTER_CLI_DEST:-/usr/local/bin/tree-sitter}" + +arch="$(uname -m)" +case "$arch" in + x86_64|amd64) ts_arch=x64 ;; + aarch64|arm64) ts_arch=arm64 ;; + *) + echo "error: unsupported arch $arch" >&2 + exit 1 + ;; +esac + +url="https://github.com/tree-sitter/tree-sitter/releases/download/v${VER}/tree-sitter-linux-${ts_arch}.gz" +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT +curl -fsSL "$url" | gunzip > "$tmp" +chmod +x "$tmp" + +if [ -w "$(dirname "$DEST")" ]; then + mv "$tmp" "$DEST" + trap - EXIT +else + sudo mv "$tmp" "$DEST" + trap - EXIT +fi + +"$DEST" --version From 793845aa05829fe52a6205c60d6c573d7933bf2c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:48:46 +0000 Subject: [PATCH 2/4] fix(codegen): restore ExprBreak/Lambda arms smashed in #757 lib/codegen_deno.ml had an unterminated comment (`wrapping Iram)`) and a duplicate `receiver_struct` (`withtion = function`) from the #757 squash. Lint failed with "Comment not terminated" at line 1409. --- lib/codegen_deno.ml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 84caf529..60dc0550 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -1407,7 +1407,14 @@ let rec gen_expr ctx (expr : expr) : string = | ExprReturn (Some e) -> iife ctx ("return " ^ gen_expr ctx e ^ ";") | ExprReturn None -> iife ctx "return Unit;" (* #459: break/continue lower to the corresponding JS keywords. The - wrapping Iram) -> mangle p.p_name.name) elam_params in + wrapping IIFE pattern used for `return` doesn't work here — JS's + `break`/`continue` only target the nearest enclosing loop and an + IIFE wraps the keyword in a new function frame. Emit a bare + statement and rely on the parent block-flatten machinery. *) + | ExprBreak _ -> iife ctx "break;" + | ExprContinue _ -> iife ctx "continue;" + | ExprLambda { elam_params; elam_body; elam_ret_ty = _ } -> + let ps = List.map (fun (p : param) -> mangle p.p_name.name) elam_params in "((" ^ String.concat ", " ps ^ ") => " ^ gen_expr ctx elam_body ^ ")" | ExprTry { et_body; et_catch; et_finally } -> gen_try ctx et_body et_catch et_finally From bfaa1746078899d8c4786c59718e40c81e8967ba Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:48:58 +0000 Subject: [PATCH 3/4] fix(codegen): drop duplicate smashed receiver_struct from #757 --- lib/codegen_deno.ml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 60dc0550..a7a36c43 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -1807,16 +1807,6 @@ let rec type_expr_name : type_expr -> string option = function | TyOwn t | TyRef (_, t) | TyMut (_, t) -> type_expr_name t | _ -> None -(* The struct (if any, among [known]) that [fd]'s first parameter is typed - as — i.e. [fd] is a receiver-first method of that struct. *) -let receiver_struct ~(known : (string, 'a) Hashtbl.t) (fd : fn_decl) - : (string * string) option = - match fd.fd_params withtion = function - | TyCon id | TyVar id -> Some id.name - | TyApp (id, _) -> Some id.name - | TyOwn t | TyRef (_, t) | TyMut (_, t) -> type_expr_name t - | _ -> None - (* The struct (if any, among [known]) that [fd]'s first parameter is typed as — i.e. [fd] is a receiver-first method of that struct. *) let receiver_struct ~(known : (string, 'a) Hashtbl.t) (fd : fn_decl) From fb7b5e45ffddb62bdc3af4fc5a2cc62d872f602b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:51:39 +0000 Subject: [PATCH 4/4] fix(ci): pin tree-sitter tarball SHA256 and force HTTPS --- scripts/install-tree-sitter-cli.sh | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/scripts/install-tree-sitter-cli.sh b/scripts/install-tree-sitter-cli.sh index 998d74dc..0af984f9 100755 --- a/scripts/install-tree-sitter-cli.sh +++ b/scripts/install-tree-sitter-cli.sh @@ -5,11 +5,12 @@ # Used by CI instead of `npm install -g --ignore-scripts tree-sitter-cli`: # that package's binary is fetched in a postinstall script, so # --ignore-scripts (Sonar S6505) leaves `tree-sitter` missing (ENOENT). -# A release tarball has no lifecycle scripts. set -euo pipefail VER="${TREE_SITTER_CLI_VERSION:-0.25.0}" -DEST="${TREE_SITTER_CLI_DEST:-/usr/local/bin/tree-sitter}" +# tree-sitter-linux-x64.gz from +# https://github.com/tree-sitter/tree-sitter/releases/tag/v0.25.0 +SHA256="${TREE_SITTER_LINUX_X64_SHA256:-d7b68a7a79459c0c23e062f719fe90781ed284a4fb172756e217ca08ea86b8d3}" arch="$(uname -m)" case "$arch" in @@ -22,17 +23,20 @@ case "$arch" in esac url="https://github.com/tree-sitter/tree-sitter/releases/download/v${VER}/tree-sitter-linux-${ts_arch}.gz" -tmp="$(mktemp)" -trap 'rm -f "$tmp"' EXIT -curl -fsSL "$url" | gunzip > "$tmp" -chmod +x "$tmp" +workdir="${RUNNER_TEMP:-$(mktemp -d)}" +archive="${workdir}/tree-sitter-linux-${ts_arch}.gz" +bin_dir="${workdir}/tree-sitter-cli" +mkdir -p "$bin_dir" -if [ -w "$(dirname "$DEST")" ]; then - mv "$tmp" "$DEST" - trap - EXIT -else - sudo mv "$tmp" "$DEST" - trap - EXIT -fi +curl --fail --location --retry 3 --proto "=https" \ + --output "$archive" \ + "$url" +printf '%s %s\n' "$SHA256" "$archive" | sha256sum --check --strict +gunzip -c "$archive" > "${bin_dir}/tree-sitter" +chmod +x "${bin_dir}/tree-sitter" -"$DEST" --version +if [[ -n "${GITHUB_PATH:-}" ]]; then + printf '%s\n' "$bin_dir" >> "$GITHUB_PATH" +fi +export PATH="${bin_dir}:${PATH}" +tree-sitter --version