@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.
Files changed (4) hide show
  1. package/README.md +25 -2
  2. package/boot.wasm +0 -0
  3. package/package.json +1 -1
  4. 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
- ## Library
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
- Requires Node.js ≥ 22.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinkle-lang/twinkle",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Twinkle — a statically typed language targeting WebAssembly GC. CLI (twk) plus an embeddable compile/run library.",
5
5
  "type": "module",
6
6
  "bin": { "twk": "node.mjs" },
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 : new TextDecoder().decode(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 { bootBytes, bridgeBytes } = await loadAssets();
62
-
63
- const files = new Map(opts.files ?? []);
64
- files.set("/input/main.tw", typeof source === "string" ? textEncoder.encode(source) : source);
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
  }