@kexhq/kex 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.mjs +109 -0
- package/dist/kex_repl_wasm.data +16927 -0
- package/dist/kex_repl_wasm.js +2 -0
- package/dist/kex_repl_wasm.wasm +0 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @kexhq/kex
|
|
2
|
+
|
|
3
|
+
The [Kex](https://github.com/kexhq/kex) language interpreter compiled to
|
|
4
|
+
WebAssembly, packaged for embedding a Kex REPL in a web page or a Node
|
|
5
|
+
script. This is the same wasm binary built and tested in the main repo
|
|
6
|
+
(`src/wasm_repl.cxx`, `web/index.html`).
|
|
7
|
+
|
|
8
|
+
Published to the public npm registry as [`@kexhq/kex`](https://www.npmjs.com/package/@kexhq/kex),
|
|
9
|
+
and mirrored to GitHub Packages for internal consumers already wired to that
|
|
10
|
+
path (e.g. kex.run's site repo).
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
**Pre-release.** The package's version follows the language's `VERSION` file:
|
|
15
|
+
|
|
16
|
+
- Every push to the repo publishes `<VERSION>-dev.<sha>` under the `next`
|
|
17
|
+
dist-tag (e.g. `0.3.0-beta.dev.a1b2c3d`) — a dev build names the version it
|
|
18
|
+
is working toward, distinguished by revision, exactly like a native
|
|
19
|
+
`kex --version` built from the same tree.
|
|
20
|
+
- A release publishes the plain `VERSION` (`0.3.0-beta`) with the same
|
|
21
|
+
dist-tag rule as the container images: `latest` for a stable release, the
|
|
22
|
+
channel name (`rc`, `beta`) for a pre-release. The `version` field in
|
|
23
|
+
`package.json` is a placeholder — the number is stamped at publish time.
|
|
24
|
+
|
|
25
|
+
Expect breaking changes without notice until 1.0.0; pin a version if you need
|
|
26
|
+
reproducible builds.
|
|
27
|
+
|
|
28
|
+
**Known limitation:** none currently — the Asyncify/JS-interop bug that
|
|
29
|
+
used to cause duplicated output and lost state for `receive`,
|
|
30
|
+
`receive timeout:`, and `Task.await` is fixed.
|
|
31
|
+
|
|
32
|
+
## Installing
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
npm install @kexhq/kex@next
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { Kex } from "@kexhq/kex";
|
|
42
|
+
|
|
43
|
+
const session = await Kex.create();
|
|
44
|
+
|
|
45
|
+
// Evaluate one chunk of Kex source at a time — state (let/var bindings,
|
|
46
|
+
// spawned processes) persists across calls on the same session, exactly
|
|
47
|
+
// like a real REPL.
|
|
48
|
+
console.log(await session.eval("1 + 2"));
|
|
49
|
+
// => "=> 3 : Int\n"
|
|
50
|
+
|
|
51
|
+
await session.eval("let x = 5");
|
|
52
|
+
console.log(await session.eval("x + 10"));
|
|
53
|
+
// => "=> 15 : Int\n"
|
|
54
|
+
|
|
55
|
+
// Tab completion — see web/index.html in the main repo for a full
|
|
56
|
+
// reference client (word-break-character scanning, history, line editing).
|
|
57
|
+
console.log(session.complete("IO.printL", 0, "IO.printL"));
|
|
58
|
+
// => ["IO.printLine"]
|
|
59
|
+
|
|
60
|
+
session.destroy();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Ships with TypeScript definitions (`Kex`, `KexModuleOptions`) — no `@types`
|
|
64
|
+
package needed, and no `new Kex(...)`: the constructor is private, use
|
|
65
|
+
`Kex.create()`.
|
|
66
|
+
|
|
67
|
+
Multi-line input (`do ... end` blocks) needs to be accumulated into one
|
|
68
|
+
string before calling `eval` — this package doesn't do that for you (see
|
|
69
|
+
`web/index.html`'s `countBlocks()` for the exact logic the real REPL uses to
|
|
70
|
+
decide when a block is complete).
|
|
71
|
+
|
|
72
|
+
Output already contains ANSI color escape codes (matching the native CLI's
|
|
73
|
+
REPL exactly) — render it through a real terminal emulator (e.g.
|
|
74
|
+
[xterm.js](https://xtermjs.org/), as `web/index.html` does) rather than
|
|
75
|
+
stripping them, unless you specifically want plain text.
|
|
76
|
+
|
|
77
|
+
## For kex.run specifically
|
|
78
|
+
|
|
79
|
+
This is the package kex.run is expected to import as its in-browser
|
|
80
|
+
interpreter. Float on `next` for development; for a deployed build pin an
|
|
81
|
+
exact version — a released one (`@kexhq/kex@0.3.0-beta`) or a specific dev
|
|
82
|
+
build (`@kexhq/kex@0.3.0-beta.dev.a1b2c3d`) — since `next` moves on every
|
|
83
|
+
push.
|
|
84
|
+
|
|
85
|
+
## Building locally
|
|
86
|
+
|
|
87
|
+
From the main repo, with `emsdk` active (pinned to 5.0.7 — see
|
|
88
|
+
`third_party/gmp-wasm/README.md`):
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
cd packages/kex
|
|
92
|
+
npm run prepack # builds build-wasm/ if needed, assembles dist/
|
|
93
|
+
npm pack # produces a .tgz you can npm install locally to test
|
|
94
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Type definitions for @kexhq/kex
|
|
2
|
+
|
|
3
|
+
/** Extra options forwarded to the underlying Emscripten module factory. */
|
|
4
|
+
export interface KexModuleOptions {
|
|
5
|
+
locateFile?: (path: string, prefix: string) => string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A Kex REPL session backed by the wasm interpreter. Internal state (the
|
|
11
|
+
* Emscripten module and the C-side session handle) is fully private —
|
|
12
|
+
* construct instances via Kex.create(), not `new Kex(...)`.
|
|
13
|
+
*/
|
|
14
|
+
export class Kex {
|
|
15
|
+
private constructor(module: unknown, session: number);
|
|
16
|
+
|
|
17
|
+
/** Creates a new, independent Kex REPL session backed by the wasm interpreter. */
|
|
18
|
+
static create(moduleOptions?: KexModuleOptions): Promise<Kex>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Evaluates one chunk of Kex source against this session's persistent
|
|
22
|
+
* interpreter state. State (`let`/`var` bindings, spawned processes)
|
|
23
|
+
* persists across calls on the same session, like a real REPL.
|
|
24
|
+
*
|
|
25
|
+
* Multi-line `do ... end` blocks must be accumulated into one string by
|
|
26
|
+
* the caller before calling eval — see web/index.html in the main repo
|
|
27
|
+
* for the exact `countBlocks()` logic the real REPL uses.
|
|
28
|
+
*
|
|
29
|
+
* The returned string already contains ANSI color escape codes (matching
|
|
30
|
+
* the native CLI's REPL exactly) — render it through a real terminal
|
|
31
|
+
* emulator (e.g. xterm.js) rather than stripping them, unless you
|
|
32
|
+
* specifically want plain text.
|
|
33
|
+
*/
|
|
34
|
+
eval(source: string): Promise<string>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tab completion, reusing the same logic the native REPL's readline
|
|
38
|
+
* integration uses. `line` is the current input line, `start` is the
|
|
39
|
+
* index where the word under the cursor begins, `text` is that word
|
|
40
|
+
* itself. Returns the list of completions (already rewritten to what
|
|
41
|
+
* should be inserted in place of `text`).
|
|
42
|
+
*/
|
|
43
|
+
complete(line: string, start: number, text: string): string[];
|
|
44
|
+
|
|
45
|
+
/** Frees this session's underlying interpreter state. Safe to call more than once. */
|
|
46
|
+
destroy(): void;
|
|
47
|
+
|
|
48
|
+
/** The REPL banner with version info, matching the native CLI exactly. */
|
|
49
|
+
banner(): string;
|
|
50
|
+
|
|
51
|
+
/** Kex version string (e.g. "0.2.0"). */
|
|
52
|
+
version(): string;
|
|
53
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Thin wrapper around the wasm REPL bindings (src/wasm_repl.cxx in the main
|
|
2
|
+
// repo) for consumption as an npm package — this is what kex.run imports to
|
|
3
|
+
// embed the interpreter. At publish time, the "prepack" build step (see
|
|
4
|
+
// ../README.md) copies this file into dist/ alongside the built
|
|
5
|
+
// kex_repl_wasm.js/.wasm/.data, so everything resolves relative to this same
|
|
6
|
+
// directory at runtime (works in both Node and a bundler, since it's plain
|
|
7
|
+
// import.meta.url-relative resolution, no cwd-dependent paths).
|
|
8
|
+
import createKexReplModule from "./kex_repl_wasm.js";
|
|
9
|
+
|
|
10
|
+
// Private fields keep the raw Emscripten Module and the C-side session
|
|
11
|
+
// pointer out of the public API entirely — callers only ever see eval/
|
|
12
|
+
// complete/destroy, never the ccall plumbing underneath.
|
|
13
|
+
export class Kex {
|
|
14
|
+
#module;
|
|
15
|
+
#session;
|
|
16
|
+
#destroyed = false;
|
|
17
|
+
|
|
18
|
+
// Not called directly — use Kex.create(), since standing up the wasm
|
|
19
|
+
// module is inherently async and a constructor can't await.
|
|
20
|
+
constructor(module, session) {
|
|
21
|
+
this.#module = module;
|
|
22
|
+
this.#session = session;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Creates a new, independent Kex REPL session backed by the wasm interpreter. */
|
|
26
|
+
static async create(moduleOptions = {}) {
|
|
27
|
+
const module = await createKexReplModule({
|
|
28
|
+
// In Node, the preload-file package (kex_repl_wasm.data) is fetched
|
|
29
|
+
// via a plain fs.readFileSync(path) call, which doesn't accept a
|
|
30
|
+
// file:// URL string — only a real filesystem path. url.pathname
|
|
31
|
+
// (not .href) gives that directly on POSIX. In a browser this needs
|
|
32
|
+
// an actual URL for fetch(), hence .href there instead.
|
|
33
|
+
locateFile: (path) => {
|
|
34
|
+
const url = new URL(path, import.meta.url);
|
|
35
|
+
return typeof window === "undefined" ? url.pathname : url.href;
|
|
36
|
+
},
|
|
37
|
+
...moduleOptions,
|
|
38
|
+
});
|
|
39
|
+
const session = module.ccall("kex_repl_create", "number", [], []);
|
|
40
|
+
return new Kex(module, session);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#assertAlive() {
|
|
44
|
+
if (this.#destroyed) throw new Error("this Kex session has already been destroyed");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Evaluates one chunk of Kex source against this session's persistent
|
|
49
|
+
* interpreter state. State (`let`/`var` bindings, spawned processes)
|
|
50
|
+
* persists across calls on the same session, like a real REPL.
|
|
51
|
+
*
|
|
52
|
+
* Multi-line `do ... end` blocks must be accumulated into one string by
|
|
53
|
+
* the caller before calling eval — see web/index.html in the main repo
|
|
54
|
+
* for the exact `countBlocks()` logic the real REPL uses.
|
|
55
|
+
*
|
|
56
|
+
* The returned string already contains ANSI color escape codes (matching
|
|
57
|
+
* the native CLI's REPL exactly) — render it through a real terminal
|
|
58
|
+
* emulator (e.g. xterm.js) rather than stripping them, unless you
|
|
59
|
+
* specifically want plain text.
|
|
60
|
+
*/
|
|
61
|
+
async eval(source) {
|
|
62
|
+
this.#assertAlive();
|
|
63
|
+
// kex_repl_eval is void-returning on purpose — see src/wasm_repl.cxx's
|
|
64
|
+
// own doc comment: a direct return value from an Asyncify-instrumented
|
|
65
|
+
// export wasn't reliably reaching the JS caller through ccall's async
|
|
66
|
+
// plumbing, so the result is fetched via a second, ordinary synchronous
|
|
67
|
+
// call instead (kex_repl_last_result never touches Asyncify).
|
|
68
|
+
await this.#module.ccall(
|
|
69
|
+
"kex_repl_eval", null,
|
|
70
|
+
["number", "string"], [this.#session, source],
|
|
71
|
+
{ async: true }
|
|
72
|
+
);
|
|
73
|
+
return this.#module.ccall("kex_repl_last_result", "string", ["number"], [this.#session]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tab completion, reusing the same logic the native REPL's readline
|
|
78
|
+
* integration uses. `line` is the current input line, `start` is the
|
|
79
|
+
* index where the word under the cursor begins, `text` is that word
|
|
80
|
+
* itself. Returns the list of completions (already rewritten to what
|
|
81
|
+
* should be inserted in place of `text`).
|
|
82
|
+
*/
|
|
83
|
+
complete(line, start, text) {
|
|
84
|
+
this.#assertAlive();
|
|
85
|
+
const raw = this.#module.ccall(
|
|
86
|
+
"kex_repl_complete", "string",
|
|
87
|
+
["number", "string", "number", "string"],
|
|
88
|
+
[this.#session, line, start, text]
|
|
89
|
+
);
|
|
90
|
+
return raw.split("\n").filter((m) => m.length > 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Frees this session's underlying interpreter state. Safe to call more than once. */
|
|
94
|
+
destroy() {
|
|
95
|
+
if (this.#destroyed) return;
|
|
96
|
+
this.#module.ccall("kex_repl_destroy", null, ["number"], [this.#session]);
|
|
97
|
+
this.#destroyed = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The REPL banner with version info, matching the native CLI exactly. */
|
|
101
|
+
banner() {
|
|
102
|
+
return this.#module.ccall("kex_repl_banner", "string", [], []);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Kex version string (e.g. "0.2.0"). */
|
|
106
|
+
version() {
|
|
107
|
+
return this.#module.ccall("kex_version", "string", [], []);
|
|
108
|
+
}
|
|
109
|
+
}
|