Skip to content

Commit b5a4109

Browse files
Codegen: add --vscode-extension flag to inline extraImports wiring (#116)
Closes #105. The Node-CJS codegen emitted a shim with an empty extraImports hook that every consumer hand-wired in a separate index.cjs to install the concrete vscode-API bindings. That boilerplate was 100% mechanical and identical across consumers. Add a --vscode-extension codegen flag that inlines the wiring so the generated .cjs is directly loadable as a VS Code extension's `main`: installs exports.extraImports calling the @hyperpolymath/affine-vscode adapter. Sub-flags --vscode-extension-adapter (override the require specifier) and --vscode-extension-no-lc (no language client; pass null) cover the documented variants. Migrate the editors/vscode pilot off the would-be hand-written index.cjs: its compile script now passes --vscode-extension and the regenerated out/extension.cjs carries the inline glue; package.json declares the adapter dependency. https://claude.ai/code/session_01FkzAgzpZFSGxzorVNZ9FUF Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72cb2db commit b5a4109

5 files changed

Lines changed: 210 additions & 9 deletions

File tree

bin/main.ml

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,8 @@ let repl_cmd_fn () =
477477
(** Compile a file. With [--json], emits diagnostics for any
478478
compilation errors. With [--wasm-gc], targets the WebAssembly GC
479479
proposal instead of WASM 1.0 linear memory. *)
480-
let compile_file face json wasm_gc path output =
480+
let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
481+
path output =
481482
let face = resolve_face ~quiet:json face path in
482483
if json then begin
483484
let diags = ref [] in
@@ -659,7 +660,13 @@ let compile_file face json wasm_gc path output =
659660
(Affinescript.Codegen.show_codegen_error e);
660661
span = Affinescript.Span.dummy; help = None; labels = [] }
661662
| Ok wasm_module ->
662-
let cjs = Affinescript.Codegen_node.emit_node_cjs wasm_module in
663+
let cjs =
664+
Affinescript.Codegen_node.emit_node_cjs
665+
~vscode_extension:vscode_ext
666+
?vscode_extension_adapter:vscode_adapter
667+
~vscode_extension_no_lc:vscode_no_lc
668+
wasm_module
669+
in
663670
let oc = open_out output in
664671
output_string oc cjs;
665672
close_out oc
@@ -873,11 +880,18 @@ let compile_file face json wasm_gc path output =
873880
(Affinescript.Codegen.show_codegen_error e);
874881
`Error (false, "Node-CJS codegen error")
875882
| Ok wasm_module ->
876-
let cjs = Affinescript.Codegen_node.emit_node_cjs wasm_module in
883+
let cjs =
884+
Affinescript.Codegen_node.emit_node_cjs
885+
~vscode_extension:vscode_ext
886+
?vscode_extension_adapter:vscode_adapter
887+
~vscode_extension_no_lc:vscode_no_lc
888+
wasm_module
889+
in
877890
let oc = open_out output in
878891
output_string oc cjs;
879892
close_out oc;
880-
Format.printf "Compiled %s -> %s (Node-CJS)@." path output;
893+
Format.printf "Compiled %s -> %s (Node-CJS%s)@." path output
894+
(if vscode_ext then ", --vscode-extension" else "");
881895
`Ok ())
882896
else
883897
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
@@ -1141,6 +1155,29 @@ let wasm_gc_arg =
11411155
Requires a runtime that supports the GC proposal: V8/Chrome ≥ 119, \
11421156
SpiderMonkey/Firefox ≥ 120, or Wasmtime with --wasm-features gc.")
11431157

1158+
(* Issue #105: --vscode-extension and its sub-flags. Only meaningful when
1159+
the output is a [.cjs] Node-CJS shim; ignored for other targets. *)
1160+
let vscode_ext_arg =
1161+
Arg.(value & flag & info ["vscode-extension"]
1162+
~doc:"When emitting a Node-CJS shim (.cjs output), inline the vscode-API \
1163+
wiring so the generated file is directly loadable as a VS Code \
1164+
extension's `main` — no hand-written index.cjs or vendored adapter. \
1165+
Installs exports.extraImports calling the \
1166+
@hyperpolymath/affine-vscode adapter.")
1167+
1168+
let vscode_adapter_arg =
1169+
Arg.(value & opt (some string) None & info ["vscode-extension-adapter"]
1170+
~docv:"SPECIFIER"
1171+
~doc:"Override the require() specifier for the vscode adapter used by \
1172+
--vscode-extension (default: @hyperpolymath/affine-vscode). Useful \
1173+
for testing against a local checkout or vendoring a custom adapter.")
1174+
1175+
let vscode_no_lc_arg =
1176+
Arg.(value & flag & info ["vscode-extension-no-lc"]
1177+
~doc:"With --vscode-extension, omit the vscode-languageclient/node \
1178+
dependency for extensions that ship no language client; the \
1179+
wiring passes null in its place.")
1180+
11441181
(** Shared --face flag: select the parser surface-syntax face. *)
11451182
let face_arg =
11461183
let faces = Arg.enum [
@@ -1486,7 +1523,9 @@ let repl_cmd =
14861523
let compile_cmd =
14871524
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
14881525
let info = Cmd.info "compile" ~doc in
1489-
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg $ path_arg $ output_arg))
1526+
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg
1527+
$ vscode_ext_arg $ vscode_adapter_arg $ vscode_no_lc_arg
1528+
$ path_arg $ output_arg))
14901529

14911530
let fmt_cmd =
14921531
let doc = "Format a file" in

editors/vscode/out/extension.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,15 @@ exports.deactivate = async function deactivate() {
9494
exports._registerHandle = _registerHandle;
9595
exports._getHandle = _getHandle;
9696
exports._freeHandle = _freeHandle;
97+
98+
// Inserted by --vscode-extension (issue #105): auto-generated glue so this
99+
// file is directly loadable as a VS Code extension's `main`. Replaces the
100+
// previously hand-written index.cjs + vendored adapter boilerplate.
101+
const _makeVscodeBindings = require("@hyperpolymath/affine-vscode");
102+
exports.extraImports = function() {
103+
return _makeVscodeBindings(
104+
require("vscode"),
105+
require("vscode-languageclient/node"),
106+
exports,
107+
);
108+
};

editors/vscode/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
},
136136
"scripts": {
137137
"vscode:prepublish": "npm run compile",
138-
"compile": "affinescript compile src/extension.affine -o out/extension.cjs",
138+
"compile": "affinescript compile src/extension.affine -o out/extension.cjs --vscode-extension",
139139
"watch": "echo 'watch mode not implemented for AffineScript source — re-run npm run compile'",
140140
"guard": "../../tools/check-no-extension-ts.sh",
141141
"package": "vsce package",
@@ -146,6 +146,7 @@
146146
"vsce": "^2.15.0"
147147
},
148148
"dependencies": {
149+
"@hyperpolymath/affine-vscode": "^0.1.0",
149150
"vscode-languageclient": "^9.0.0"
150151
}
151152
}

lib/codegen_node.ml

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,85 @@ let encode_module_to_bytes (m : wasm_module) : bytes =
9696
) m.custom_sections;
9797
Bytes.of_string (Buffer.contents buf)
9898

99+
(** Escape a string for embedding inside a JS double-quoted literal. Only
100+
the characters that would break out of (or corrupt) the literal are
101+
escaped — sufficient for require() specifiers and module paths. *)
102+
let js_string_escape (s : string) : string =
103+
let buf = Buffer.create (String.length s + 8) in
104+
String.iter (fun c ->
105+
match c with
106+
| '\\' -> Buffer.add_string buf "\\\\"
107+
| '"' -> Buffer.add_string buf "\\\""
108+
| '\n' -> Buffer.add_string buf "\\n"
109+
| '\r' -> Buffer.add_string buf "\\r"
110+
| c -> Buffer.add_char buf c
111+
) s;
112+
Buffer.contents buf
113+
114+
(** Default npm specifier for the vscode-API adapter (issue #105). Callers
115+
can override it via [~vscode_extension_adapter]. *)
116+
let default_vscode_adapter = "@hyperpolymath/affine-vscode"
117+
118+
(** Build the [--vscode-extension] wiring block (issue #105).
119+
120+
Returns the JS that installs [exports.extraImports] so the generated
121+
[.cjs] is directly loadable as a VS Code extension's [main] — no
122+
hand-written [index.cjs], no vendored adapter. [adapter] is the
123+
require() specifier for the adapter; when [no_lc] is set the extension
124+
ships no language client, so the [vscode-languageclient/node] require
125+
is skipped and [null] is passed in its place. *)
126+
let vscode_extension_wiring ~(adapter : string) ~(no_lc : bool) : string =
127+
let lc_arg =
128+
if no_lc then "null"
129+
else {|require("vscode-languageclient/node")|}
130+
in
131+
Printf.sprintf {|
132+
// Inserted by --vscode-extension (issue #105): auto-generated glue so this
133+
// file is directly loadable as a VS Code extension's `main`. Replaces the
134+
// previously hand-written index.cjs + vendored adapter boilerplate.
135+
const _makeVscodeBindings = require("%s");
136+
exports.extraImports = function() {
137+
return _makeVscodeBindings(
138+
require("vscode"),
139+
%s,
140+
exports,
141+
);
142+
};
143+
|} (js_string_escape adapter) lc_arg
144+
99145
(** Wrap [m] in a Node-CJS shim. The shim is a single self-contained
100-
JavaScript string suitable for writing to a [.cjs] file. *)
101-
let emit_node_cjs ?(extra_imports_js : string option) (m : wasm_module) : string =
146+
JavaScript string suitable for writing to a [.cjs] file.
147+
148+
When [~vscode_extension:true] (issue #105), the shim additionally
149+
installs [exports.extraImports] inline so the output is directly
150+
loadable as a VS Code extension's [main]. [~vscode_extension_adapter]
151+
overrides the adapter require() specifier (default
152+
{!default_vscode_adapter}); [~vscode_extension_no_lc] omits the
153+
[vscode-languageclient/node] dependency for extensions that ship no
154+
language client. *)
155+
let emit_node_cjs
156+
?(extra_imports_js : string option)
157+
?(vscode_extension : bool = false)
158+
?(vscode_extension_adapter : string option)
159+
?(vscode_extension_no_lc : bool = false)
160+
(m : wasm_module) : string =
102161
let wasm_bytes = encode_module_to_bytes m in
103162
let b64 = base64_encode wasm_bytes in
104163
let extra =
105164
match extra_imports_js with
106165
| Some js -> js
107166
| None -> "{}"
108167
in
168+
let vscode_block =
169+
if vscode_extension then
170+
let adapter =
171+
match vscode_extension_adapter with
172+
| Some a -> a
173+
| None -> default_vscode_adapter
174+
in
175+
vscode_extension_wiring ~adapter ~no_lc:vscode_extension_no_lc
176+
else ""
177+
in
109178
Printf.sprintf {|// Generated by AffineScript. Do not edit.
110179
// Node-CJS shim wrapping the compiled .wasm module so VS Code's extension
111180
// host (or any CJS consumer) can require() it directly.
@@ -202,4 +271,4 @@ exports.deactivate = async function deactivate() {
202271
exports._registerHandle = _registerHandle;
203272
exports._getHandle = _getHandle;
204273
exports._freeHandle = _freeHandle;
205-
|} b64 extra
274+
%s|} b64 extra vscode_block

test/test_e2e.ml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,9 +2822,89 @@ let test_node_cjs_base64_roundtrip () =
28222822
Alcotest.(check string) "matches RFC 4648 §10 vector"
28232823
"TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu" b64
28242824

2825+
(* ---- Issue #105: --vscode-extension inline wiring ----
2826+
2827+
Verifies that emit_node_cjs ~vscode_extension:true inlines the
2828+
exports.extraImports glue (calling the @hyperpolymath/affine-vscode
2829+
adapter) so the generated .cjs is directly loadable as a VS Code
2830+
extension's `main` — no hand-written index.cjs / vendored adapter. *)
2831+
2832+
let contains s sub =
2833+
let n = String.length sub and m = String.length s in
2834+
let rec scan i = i + n <= m && (String.sub s i n = sub || scan (i + 1)) in
2835+
n = 0 || scan 0
2836+
2837+
let cjs_of ?vscode_extension ?vscode_extension_adapter
2838+
?vscode_extension_no_lc src =
2839+
let prog = parse_string_for_gc src in
2840+
match Codegen.generate_module prog with
2841+
| Error e -> Alcotest.failf "wasm codegen failed: %s" (Codegen.show_codegen_error e)
2842+
| Ok wasm_module ->
2843+
Codegen_node.emit_node_cjs ?vscode_extension ?vscode_extension_adapter
2844+
?vscode_extension_no_lc wasm_module
2845+
2846+
let activate_src =
2847+
{|pub fn activate(ctx_handle: Int) -> Int { return 0; }
2848+
pub fn deactivate() -> Int { return 0; }|}
2849+
2850+
let test_vscode_extension_off_by_default () =
2851+
let cjs = cjs_of activate_src in
2852+
Alcotest.(check bool) "no extraImports assignment without the flag"
2853+
false (contains cjs "exports.extraImports = function");
2854+
Alcotest.(check bool) "no adapter require without the flag"
2855+
false (contains cjs "@hyperpolymath/affine-vscode")
2856+
2857+
let test_vscode_extension_inlines_wiring () =
2858+
let cjs = cjs_of ~vscode_extension:true activate_src in
2859+
Alcotest.(check bool) "installs exports.extraImports"
2860+
true (contains cjs "exports.extraImports = function");
2861+
Alcotest.(check bool) "requires the default adapter"
2862+
true (contains cjs {|require("@hyperpolymath/affine-vscode")|});
2863+
Alcotest.(check bool) "requires the vscode module"
2864+
true (contains cjs {|require("vscode")|});
2865+
Alcotest.(check bool) "requires the language client"
2866+
true (contains cjs {|require("vscode-languageclient/node")|});
2867+
Alcotest.(check bool) "passes exports as the host shim"
2868+
true (contains cjs "exports,\n );");
2869+
(* The base shim (use strict, activate, handle table) is still present. *)
2870+
Alcotest.(check bool) "base shim still intact"
2871+
true (contains cjs "exports._registerHandle")
2872+
2873+
let test_vscode_extension_adapter_override () =
2874+
let cjs = cjs_of ~vscode_extension:true
2875+
~vscode_extension_adapter:"../local/adapter.cjs" activate_src in
2876+
Alcotest.(check bool) "uses the overridden adapter specifier"
2877+
true (contains cjs {|require("../local/adapter.cjs")|});
2878+
Alcotest.(check bool) "does not fall back to the default adapter"
2879+
false (contains cjs "@hyperpolymath/affine-vscode")
2880+
2881+
let test_vscode_extension_no_lc () =
2882+
let cjs = cjs_of ~vscode_extension:true ~vscode_extension_no_lc:true
2883+
activate_src in
2884+
Alcotest.(check bool) "skips the language-client require"
2885+
false (contains cjs "vscode-languageclient/node");
2886+
Alcotest.(check bool) "passes null in its place"
2887+
true (contains cjs " null,\n");
2888+
Alcotest.(check bool) "still requires vscode + adapter"
2889+
true (contains cjs {|require("vscode")|}
2890+
&& contains cjs {|require("@hyperpolymath/affine-vscode")|})
2891+
2892+
let test_vscode_extension_adapter_escaped () =
2893+
(* A specifier containing a double quote must not break out of the JS
2894+
string literal. *)
2895+
let cjs = cjs_of ~vscode_extension:true
2896+
~vscode_extension_adapter:{|a"b\c|} activate_src in
2897+
Alcotest.(check bool) "double quote and backslash are escaped"
2898+
true (contains cjs {|require("a\"b\\c")|})
2899+
28252900
let codegen_node_tests = [
28262901
Alcotest.test_case "Node-CJS shim has all anchors (use strict, exports.activate, ...)" `Quick test_node_cjs_shim_shape;
28272902
Alcotest.test_case "base64 encoder matches RFC 4648 vector" `Quick test_node_cjs_base64_roundtrip;
2903+
Alcotest.test_case "--vscode-extension off by default" `Quick test_vscode_extension_off_by_default;
2904+
Alcotest.test_case "--vscode-extension inlines extraImports wiring" `Quick test_vscode_extension_inlines_wiring;
2905+
Alcotest.test_case "--vscode-extension-adapter overrides the require specifier" `Quick test_vscode_extension_adapter_override;
2906+
Alcotest.test_case "--vscode-extension-no-lc skips the language client" `Quick test_vscode_extension_no_lc;
2907+
Alcotest.test_case "--vscode-extension-adapter specifier is JS-escaped" `Quick test_vscode_extension_adapter_escaped;
28282908
]
28292909

28302910
(* ---- Stdlib parse + Core import regression ----

0 commit comments

Comments
 (0)