@twinkle-lang/twinkle 0.6.2 → 0.7.1
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 +25 -2
- package/boot.wasm +0 -0
- package/package.json +1 -1
- package/web.mjs +136 -16
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ npx twk build path/to/program.tw -o out.wasm
|
|
|
18
18
|
npx twk fmt path/to/program.tw
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Node library
|
|
22
22
|
|
|
23
23
|
```js
|
|
24
24
|
import { compile, run, runFile } from "@twinkle-lang/twinkle";
|
|
@@ -41,4 +41,27 @@ await run(wasm, { imports: { canvas } });
|
|
|
41
41
|
|
|
42
42
|
A missing extern import produces a clear error naming the exact `module.fn`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
## Browser library
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { command, run, load } from "@twinkle-lang/twinkle/web";
|
|
48
|
+
|
|
49
|
+
await load(); // optional prefetch
|
|
50
|
+
|
|
51
|
+
const result = await command(["fmt", "/input/main.tw"], {
|
|
52
|
+
source: editor.getValue(),
|
|
53
|
+
env: { NO_COLOR: "1" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (result.exitCode === 0) {
|
|
57
|
+
editor.setValue(result.text("/input/main.tw"));
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`command(args, opts)` runs the shipped compiler payload against an in-memory
|
|
62
|
+
filesystem and returns `{ exitCode, stdout, stderr, files, text(path), bytes(path) }`.
|
|
63
|
+
Compiler failures are returned as non-zero exit codes; host/runtime failures throw.
|
|
64
|
+
The existing browser `run(source, opts)` helper is still available and returns an
|
|
65
|
+
exit code.
|
|
66
|
+
|
|
67
|
+
Requires Node.js ≥ 22 for the Node APIs. Browser APIs require WebAssembly GC.
|
package/boot.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/web.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Browser entry for embedding Twinkle.
|
|
2
2
|
//
|
|
3
|
-
// import { run } from "@twinkle-lang/twinkle/web";
|
|
3
|
+
// import { command, run } from "@twinkle-lang/twinkle/web";
|
|
4
|
+
// await command(["fmt", "/input/main.tw"], { source });
|
|
4
5
|
// await run(source, { stdout, stderr });
|
|
5
6
|
//
|
|
6
7
|
// Unlike index.mjs (Node: temp files, node:fs), this is browser-first: it
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
import { runWasmBytesAsync, createMemoryHost } from "./runtime.mjs";
|
|
12
13
|
|
|
13
14
|
const textEncoder = new TextEncoder();
|
|
15
|
+
const textDecoder = new TextDecoder();
|
|
14
16
|
|
|
15
17
|
// boot.wasm / bridge.wasm are published next to this module. `new URL(...,
|
|
16
18
|
// import.meta.url)` lets the consumer's bundler emit them as assets (Vite,
|
|
@@ -37,11 +39,135 @@ export function load() {
|
|
|
37
39
|
|
|
38
40
|
const defaultStream = (sink) => ({
|
|
39
41
|
write(chunk) {
|
|
40
|
-
sink(typeof chunk === "string" ? chunk :
|
|
42
|
+
sink(typeof chunk === "string" ? chunk : textDecoder.decode(chunk));
|
|
41
43
|
return true;
|
|
42
44
|
},
|
|
43
45
|
});
|
|
44
46
|
|
|
47
|
+
function assertPath(path, label) {
|
|
48
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
49
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bytesFor(value, label) {
|
|
54
|
+
if (typeof value === "string") return textEncoder.encode(value);
|
|
55
|
+
if (value instanceof Uint8Array) return value;
|
|
56
|
+
throw new TypeError(`${label} must be a string or Uint8Array`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createCaptureStream(forward) {
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
let text = "";
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
stream: {
|
|
65
|
+
write(chunk) {
|
|
66
|
+
if (typeof chunk === "string") {
|
|
67
|
+
text += decoder.decode();
|
|
68
|
+
text += chunk;
|
|
69
|
+
} else {
|
|
70
|
+
text += decoder.decode(chunk, { stream: true });
|
|
71
|
+
}
|
|
72
|
+
if (forward) forward.write(chunk);
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
finish() {
|
|
77
|
+
text += decoder.decode();
|
|
78
|
+
return text;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function seedFile(host, cwd, path, value, label) {
|
|
84
|
+
assertPath(path, label);
|
|
85
|
+
if (typeof host.resolvePath !== "function" || typeof host.writeBytes !== "function") {
|
|
86
|
+
throw new TypeError("command: host must provide resolvePath(cwd, path) and writeBytes(path, bytes)");
|
|
87
|
+
}
|
|
88
|
+
host.writeBytes(host.resolvePath(cwd, path), bytesFor(value, label));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeArgs(args) {
|
|
92
|
+
if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
|
|
93
|
+
throw new TypeError("command: args must be an array of strings");
|
|
94
|
+
}
|
|
95
|
+
return args.slice();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function commandResult(exitCode, stdout, stderr, host, cwd) {
|
|
99
|
+
const files = host.files instanceof Map ? host.files : new Map();
|
|
100
|
+
return {
|
|
101
|
+
exitCode,
|
|
102
|
+
stdout,
|
|
103
|
+
stderr,
|
|
104
|
+
files,
|
|
105
|
+
text(path) {
|
|
106
|
+
const data = this.bytes(path);
|
|
107
|
+
return data === undefined ? undefined : textDecoder.decode(data);
|
|
108
|
+
},
|
|
109
|
+
bytes(path) {
|
|
110
|
+
assertPath(path, "path");
|
|
111
|
+
const normalized = typeof host.resolvePath === "function" ? host.resolvePath(cwd, path) : path;
|
|
112
|
+
return files.get(normalized);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Run a compiler command in the browser against an in-memory project.
|
|
119
|
+
*
|
|
120
|
+
* @param {string[]} args CLI arguments, e.g. `["fmt", "/input/main.tw"]`.
|
|
121
|
+
* @param {object} [opts]
|
|
122
|
+
* @param {string | Uint8Array} [opts.source] Entry source written to opts.path.
|
|
123
|
+
* @param {string} [opts.path] Path for opts.source; defaults to /input/main.tw.
|
|
124
|
+
* @param {Iterable<[string,string | Uint8Array]>} [opts.files] Files to seed first.
|
|
125
|
+
* @param {string} [opts.cwd] Working directory; defaults to /.
|
|
126
|
+
* @param {object} [opts.stdout] Optional stream with write(chunk), tee'd with capture.
|
|
127
|
+
* @param {object} [opts.stderr] Optional stream with write(chunk), tee'd with capture.
|
|
128
|
+
* @param {object} [opts.env] Environment map exposed to the compiler/program.
|
|
129
|
+
* @param {object} [opts.imports] Extern imports for commands that execute user Wasm.
|
|
130
|
+
* @param {object} [opts.host] Override the host adapter entirely (advanced).
|
|
131
|
+
* @returns {Promise<{exitCode:number, stdout:string, stderr:string, files:Map<string,Uint8Array>, text:function, bytes:function}>}
|
|
132
|
+
*/
|
|
133
|
+
export async function command(args, opts = {}) {
|
|
134
|
+
const guestArgs = normalizeArgs(args);
|
|
135
|
+
const cwd = opts.cwd ?? "/";
|
|
136
|
+
assertPath(cwd, "cwd");
|
|
137
|
+
|
|
138
|
+
const { bootBytes, bridgeBytes } = await loadAssets();
|
|
139
|
+
const host = opts.host ?? createMemoryHost();
|
|
140
|
+
|
|
141
|
+
if (opts.files !== undefined) {
|
|
142
|
+
for (const entry of opts.files) {
|
|
143
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
144
|
+
throw new TypeError("command: files must be an iterable of [path, contents] pairs");
|
|
145
|
+
}
|
|
146
|
+
seedFile(host, cwd, entry[0], entry[1], "file path");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (opts.source !== undefined) {
|
|
151
|
+
seedFile(host, cwd, opts.path ?? "/input/main.tw", opts.source, "path");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stdoutCapture = createCaptureStream(opts.stdout);
|
|
155
|
+
const stderrCapture = createCaptureStream(opts.stderr);
|
|
156
|
+
const exitCode = await runWasmBytesAsync(bootBytes, {
|
|
157
|
+
programPath: "twk.wasm",
|
|
158
|
+
guestArgs,
|
|
159
|
+
cwd,
|
|
160
|
+
env: opts.env ?? {},
|
|
161
|
+
stdout: stdoutCapture.stream,
|
|
162
|
+
stderr: stderrCapture.stream,
|
|
163
|
+
bridgeBytes,
|
|
164
|
+
host,
|
|
165
|
+
imports: opts.imports ?? {},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return commandResult(exitCode, stdoutCapture.finish(), stderrCapture.finish(), host, cwd);
|
|
169
|
+
}
|
|
170
|
+
|
|
45
171
|
/**
|
|
46
172
|
* Compile and run Twinkle source in the browser.
|
|
47
173
|
*
|
|
@@ -53,25 +179,19 @@ const defaultStream = (sink) => ({
|
|
|
53
179
|
* @param {object} [opts.imports] Extern imports — `module → fn | { fn?, args? }`
|
|
54
180
|
* (else resolved via globalThis). `args` is the per-arg spec, e.g.
|
|
55
181
|
* `{ canvas: { fill_rect: { fn, args: ['raw','raw','raw','raw','raw'] } } }`.
|
|
56
|
-
* @param {Iterable<[string,Uint8Array]>} [opts.files] Extra in-memory files (multi-file projects).
|
|
182
|
+
* @param {Iterable<[string,string | Uint8Array]>} [opts.files] Extra in-memory files (multi-file projects).
|
|
183
|
+
* @param {string} [opts.path] Entry path; defaults to /input/main.tw.
|
|
57
184
|
* @param {object} [opts.host] Override the host adapter entirely (advanced).
|
|
58
185
|
* @returns {Promise<number>} exit code
|
|
59
186
|
*/
|
|
60
187
|
export async function run(source, opts = {}) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return runWasmBytesAsync(bootBytes, {
|
|
67
|
-
programPath: "twk.wasm",
|
|
68
|
-
guestArgs: ["run", "/input/main.tw"],
|
|
69
|
-
cwd: "/",
|
|
70
|
-
env: opts.env ?? {},
|
|
188
|
+
const path = opts.path ?? "/input/main.tw";
|
|
189
|
+
const result = await command(["run", path], {
|
|
190
|
+
...opts,
|
|
191
|
+
source,
|
|
192
|
+
path,
|
|
71
193
|
stdout: opts.stdout ?? defaultStream((s) => console.log(s)),
|
|
72
194
|
stderr: opts.stderr ?? defaultStream((s) => console.error(s)),
|
|
73
|
-
bridgeBytes,
|
|
74
|
-
host: opts.host ?? createMemoryHost(files),
|
|
75
|
-
imports: opts.imports ?? {},
|
|
76
195
|
});
|
|
196
|
+
return result.exitCode;
|
|
77
197
|
}
|