Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/affine-vscode-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# Publishes @hyperpolymath/affine-vscode to npm on a scoped tag push.
#
# This repo is Deno-first (see CLAUDE.md). The npm publish here is a
# deliberate, owner-sanctioned exception (issue #104): the VS Code
# extension host is npm-native and cannot consume the Deno/JSR manifest,
# so AffineScript-authored extensions resolve the adapter via
# `require("@hyperpolymath/affine-vscode")`. Only the publish step touches
# npm; no npm runtime deps are added to the repo and no .npmrc is
# committed (the auth file is written to $HOME at runtime).
#
# Trigger: push a tag matching `affine-vscode-v*` (e.g. affine-vscode-v0.1.0).
# This pattern is intentionally distinct from the `v*` tags used by the
# OCaml compiler Release workflow so the two never collide.
#
# Requires repository secret NPM_TOKEN (an npm automation token with
# publish rights to the @hyperpolymath scope).

name: Publish affine-vscode

on:
push:
tags:
- 'affine-vscode-v*'

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Verify tag matches package version
working-directory: packages/affine-vscode
run: |
tag="${GITHUB_REF_NAME#affine-vscode-v}"
pkg="$(node -p "require('./package.json').version")"
if [ "$tag" != "$pkg" ]; then
echo "❌ Tag version ($tag) does not match package.json version ($pkg)" >&2
exit 1
fi
echo "✅ Publishing @hyperpolymath/affine-vscode@$pkg"

- name: Configure npm auth
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "${NPM_TOKEN}" ]; then
echo "❌ NPM_TOKEN secret is not set" >&2
exit 1
fi
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "${HOME}/.npmrc"

- name: Publish to npm
working-directory: packages/affine-vscode
run: npm publish --access public

- name: Clean up npm auth
if: always()
run: rm -f "${HOME}/.npmrc"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Thumbs.db
# Build
/target/
/_build/
/_opam/
/build/
/dist/
/out/
Expand Down
49 changes: 44 additions & 5 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ let repl_cmd_fn () =
(** Compile a file. With [--json], emits diagnostics for any
compilation errors. With [--wasm-gc], targets the WebAssembly GC
proposal instead of WASM 1.0 linear memory. *)
let compile_file face json wasm_gc path output =
let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
path output =
let face = resolve_face ~quiet:json face path in
if json then begin
let diags = ref [] in
Expand Down Expand Up @@ -659,7 +660,13 @@ let compile_file face json wasm_gc path output =
(Affinescript.Codegen.show_codegen_error e);
span = Affinescript.Span.dummy; help = None; labels = [] }
| Ok wasm_module ->
let cjs = Affinescript.Codegen_node.emit_node_cjs wasm_module in
let cjs =
Affinescript.Codegen_node.emit_node_cjs
~vscode_extension:vscode_ext
?vscode_extension_adapter:vscode_adapter
~vscode_extension_no_lc:vscode_no_lc
wasm_module
in
let oc = open_out output in
output_string oc cjs;
close_out oc
Expand Down Expand Up @@ -873,11 +880,18 @@ let compile_file face json wasm_gc path output =
(Affinescript.Codegen.show_codegen_error e);
`Error (false, "Node-CJS codegen error")
| Ok wasm_module ->
let cjs = Affinescript.Codegen_node.emit_node_cjs wasm_module in
let cjs =
Affinescript.Codegen_node.emit_node_cjs
~vscode_extension:vscode_ext
?vscode_extension_adapter:vscode_adapter
~vscode_extension_no_lc:vscode_no_lc
wasm_module
in
let oc = open_out output in
output_string oc cjs;
close_out oc;
Format.printf "Compiled %s -> %s (Node-CJS)@." path output;
Format.printf "Compiled %s -> %s (Node-CJS%s)@." path output
(if vscode_ext then ", --vscode-extension" else "");
`Ok ())
else
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
Expand Down Expand Up @@ -1141,6 +1155,29 @@ let wasm_gc_arg =
Requires a runtime that supports the GC proposal: V8/Chrome ≥ 119, \
SpiderMonkey/Firefox ≥ 120, or Wasmtime with --wasm-features gc.")

(* Issue #105: --vscode-extension and its sub-flags. Only meaningful when
the output is a [.cjs] Node-CJS shim; ignored for other targets. *)
let vscode_ext_arg =
Arg.(value & flag & info ["vscode-extension"]
~doc:"When emitting a Node-CJS shim (.cjs output), inline the vscode-API \
wiring so the generated file is directly loadable as a VS Code \
extension's `main` — no hand-written index.cjs or vendored adapter. \
Installs exports.extraImports calling the \
@hyperpolymath/affine-vscode adapter.")

let vscode_adapter_arg =
Arg.(value & opt (some string) None & info ["vscode-extension-adapter"]
~docv:"SPECIFIER"
~doc:"Override the require() specifier for the vscode adapter used by \
--vscode-extension (default: @hyperpolymath/affine-vscode). Useful \
for testing against a local checkout or vendoring a custom adapter.")

let vscode_no_lc_arg =
Arg.(value & flag & info ["vscode-extension-no-lc"]
~doc:"With --vscode-extension, omit the vscode-languageclient/node \
dependency for extensions that ship no language client; the \
wiring passes null in its place.")

(** Shared --face flag: select the parser surface-syntax face. *)
let face_arg =
let faces = Arg.enum [
Expand Down Expand Up @@ -1486,7 +1523,9 @@ let repl_cmd =
let compile_cmd =
let doc = "Compile a file to WebAssembly (1.0 or GC proposal), Julia (.jl), JavaScript (.js), C (.c), a WGSL compute kernel (.wgsl), a Faust DSP program (.dsp), or an ONNX model (.onnx)" in
let info = Cmd.info "compile" ~doc in
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg $ path_arg $ output_arg))
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg
$ vscode_ext_arg $ vscode_adapter_arg $ vscode_no_lc_arg
$ path_arg $ output_arg))

let fmt_cmd =
let doc = "Format a file" in
Expand Down
12 changes: 12 additions & 0 deletions editors/vscode/out/extension.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,15 @@ exports.deactivate = async function deactivate() {
exports._registerHandle = _registerHandle;
exports._getHandle = _getHandle;
exports._freeHandle = _freeHandle;

// Inserted by --vscode-extension (issue #105): auto-generated glue so this
// file is directly loadable as a VS Code extension's `main`. Replaces the
// previously hand-written index.cjs + vendored adapter boilerplate.
const _makeVscodeBindings = require("@hyperpolymath/affine-vscode");
exports.extraImports = function() {
return _makeVscodeBindings(
require("vscode"),
require("vscode-languageclient/node"),
exports,
);
};
3 changes: 2 additions & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "affinescript compile src/extension.affine -o out/extension.cjs",
"compile": "affinescript compile src/extension.affine -o out/extension.cjs --vscode-extension",
"watch": "echo 'watch mode not implemented for AffineScript source — re-run npm run compile'",
"guard": "../../tools/check-no-extension-ts.sh",
"package": "vsce package",
Expand All @@ -146,6 +146,7 @@
"vsce": "^2.15.0"
},
"dependencies": {
"@hyperpolymath/affine-vscode": "^0.1.0",
"vscode-languageclient": "^9.0.0"
}
}
75 changes: 72 additions & 3 deletions lib/codegen_node.ml
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,85 @@ let encode_module_to_bytes (m : wasm_module) : bytes =
) m.custom_sections;
Bytes.of_string (Buffer.contents buf)

(** Escape a string for embedding inside a JS double-quoted literal. Only
the characters that would break out of (or corrupt) the literal are
escaped — sufficient for require() specifiers and module paths. *)
let js_string_escape (s : string) : string =
let buf = Buffer.create (String.length s + 8) in
String.iter (fun c ->
match c with
| '\\' -> Buffer.add_string buf "\\\\"
| '"' -> Buffer.add_string buf "\\\""
| '\n' -> Buffer.add_string buf "\\n"
| '\r' -> Buffer.add_string buf "\\r"
| c -> Buffer.add_char buf c
) s;
Buffer.contents buf

(** Default npm specifier for the vscode-API adapter (issue #105). Callers
can override it via [~vscode_extension_adapter]. *)
let default_vscode_adapter = "@hyperpolymath/affine-vscode"

(** Build the [--vscode-extension] wiring block (issue #105).

Returns the JS that installs [exports.extraImports] so the generated
[.cjs] is directly loadable as a VS Code extension's [main] — no
hand-written [index.cjs], no vendored adapter. [adapter] is the
require() specifier for the adapter; when [no_lc] is set the extension
ships no language client, so the [vscode-languageclient/node] require
is skipped and [null] is passed in its place. *)
let vscode_extension_wiring ~(adapter : string) ~(no_lc : bool) : string =
let lc_arg =
if no_lc then "null"
else {|require("vscode-languageclient/node")|}
in
Printf.sprintf {|
// Inserted by --vscode-extension (issue #105): auto-generated glue so this
// file is directly loadable as a VS Code extension's `main`. Replaces the
// previously hand-written index.cjs + vendored adapter boilerplate.
const _makeVscodeBindings = require("%s");
exports.extraImports = function() {
return _makeVscodeBindings(
require("vscode"),
%s,
exports,
);
};
|} (js_string_escape adapter) lc_arg

(** Wrap [m] in a Node-CJS shim. The shim is a single self-contained
JavaScript string suitable for writing to a [.cjs] file. *)
let emit_node_cjs ?(extra_imports_js : string option) (m : wasm_module) : string =
JavaScript string suitable for writing to a [.cjs] file.

When [~vscode_extension:true] (issue #105), the shim additionally
installs [exports.extraImports] inline so the output is directly
loadable as a VS Code extension's [main]. [~vscode_extension_adapter]
overrides the adapter require() specifier (default
{!default_vscode_adapter}); [~vscode_extension_no_lc] omits the
[vscode-languageclient/node] dependency for extensions that ship no
language client. *)
let emit_node_cjs
?(extra_imports_js : string option)
?(vscode_extension : bool = false)
?(vscode_extension_adapter : string option)
?(vscode_extension_no_lc : bool = false)
(m : wasm_module) : string =
let wasm_bytes = encode_module_to_bytes m in
let b64 = base64_encode wasm_bytes in
let extra =
match extra_imports_js with
| Some js -> js
| None -> "{}"
in
let vscode_block =
if vscode_extension then
let adapter =
match vscode_extension_adapter with
| Some a -> a
| None -> default_vscode_adapter
in
vscode_extension_wiring ~adapter ~no_lc:vscode_extension_no_lc
else ""
in
Printf.sprintf {|// Generated by AffineScript. Do not edit.
// Node-CJS shim wrapping the compiled .wasm module so VS Code's extension
// host (or any CJS consumer) can require() it directly.
Expand Down Expand Up @@ -202,4 +271,4 @@ exports.deactivate = async function deactivate() {
exports._registerHandle = _registerHandle;
exports._getHandle = _getHandle;
exports._freeHandle = _freeHandle;
|} b64 extra
%s|} b64 extra vscode_block
52 changes: 38 additions & 14 deletions packages/affine-vscode/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,54 @@ JS-side adapter for the `stdlib/Vscode.affine` and
`stdlib/VscodeLanguageClient.affine` binding modules. Issue #35 Phase 2
deliverable.

== Install

[source,sh]
----
npm install @hyperpolymath/affine-vscode
----

Add it to your extension's `package.json` `dependencies`. `vscode` and
`vscode-languageclient` are peer dependencies (the latter is optional —
omit it with `--vscode-extension-no-lc`).

== Usage

In your AffineScript-authored VS Code extension's CJS entry point, after
loading the `.cjs` produced by `affinescript compile extension.affine
-o extension.cjs`, plug this adapter into the Wasm imports:
=== Recommended: `--vscode-extension` (issue #105)

Compile with the `--vscode-extension` flag and the generated `.cjs` is
directly loadable as the extension's `main` — the adapter wiring is
emitted inline, so there is no hand-written entry point to maintain:

[source,sh]
----
affinescript compile src/extension.affine -o out/extension.cjs --vscode-extension
----

Point your extension's `package.json` `main` at the unmodified
`out/extension.cjs` and list `@hyperpolymath/affine-vscode` in
`dependencies`. Sub-flags: `--vscode-extension-adapter <specifier>`
overrides the adapter `require()` target; `--vscode-extension-no-lc`
omits the `vscode-languageclient/node` dependency.

=== Manual wiring (fallback)

If you cannot use the flag, set the shim's `extraImports` hook before the
first `activate`/`deactivate` call so the extern fns resolve to live
vscode APIs. Pass the shim module itself as the third argument — the
adapter reads its handle table and `_instance` lazily:

[source,javascript]
----
const vscode = require("vscode");
const lc = require("vscode-languageclient/node");
const makeBindings = require("@hyperpolymath/affine-vscode");

// The Node-CJS shim emitted by affinescript's compile -o *.cjs path lazily
// instantiates the wasm. Patch its `_extraImports()` hook before the first
// activate/deactivate call so the extern fns resolve to live vscode APIs.

let instance;
const adapter = require("./extension.cjs");
const original = adapter._extraImports || (() => ({}));
adapter._extraImports = () => makeBindings(vscode, lc, () => instance);
const shim = require("./extension.cjs");
shim.extraImports = () => makeBindings(vscode, lc, shim);

// (Real wiring belongs in the codegen — Phase 3 will replace this manual
// hookup with auto-generated glue once `extension.affine` is the source of
// truth for the extension entry point.)
exports.activate = shim.activate;
exports.deactivate = shim.deactivate;
----

== Surface
Expand Down
17 changes: 10 additions & 7 deletions packages/affine-vscode/mod.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
//
// affine-vscode: JS-side adapter for stdlib/Vscode.affine + stdlib/VscodeLanguageClient.affine.
//
// Issue #35 Phase 2 deliverable. Plug this into your Node-CJS extension's
// `_extraImports()` so each `extern fn` declared in the bindings resolves
// to the right vscode API call.
// Issue #35 Phase 2 deliverable. Resolves each `extern fn` declared in the
// bindings to the right vscode API call.
//
// Usage from a hand-written .cjs (until Phase 3 automates the wiring):
// Preferred wiring (issue #105): compile with `--vscode-extension` and the
// generated .cjs installs `exports.extraImports` calling this adapter
// automatically — no hand-written entry point.
//
// const vscodeBindings = require("@hyperpolymath/affine-vscode")(
// Manual wiring (fallback), from a hand-written .cjs:
//
// const shim = require("./extension.cjs");
// shim.extraImports = () => require("@hyperpolymath/affine-vscode")(
// require("vscode"),
// require("vscode-languageclient/node"),
// instance, // WebAssembly.Instance, set after instantiate
// shim, // the .cjs shim module (hostShim)
// );
// // Pass vscodeBindings into the Wasm imports map under "env".
//
// The adapter maintains a per-process JS-side handle table keyed by Int
// so opaque handles passed across the FFI boundary survive round-trips.
Expand Down
Loading
Loading