wasm-idle 0.4.1 → 0.5.0

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 (39) hide show
  1. package/README.md +94 -3
  2. package/dist/clang/app.d.ts +4 -0
  3. package/dist/clang/app.js +315 -12
  4. package/dist/clang/index.d.ts +4 -0
  5. package/dist/clang/index.js +151 -8
  6. package/dist/debug/controller.svelte.d.ts +43 -0
  7. package/dist/debug/controller.svelte.js +268 -0
  8. package/dist/debug/editor/index.d.ts +2 -1
  9. package/dist/debug/editor/index.js +1 -1
  10. package/dist/debug/editor/monaco.d.ts +8 -0
  11. package/dist/debug/editor/monaco.js +23 -0
  12. package/dist/debug/expression.d.ts +24 -0
  13. package/dist/debug/expression.js +307 -138
  14. package/dist/debug/index.d.ts +4 -1
  15. package/dist/debug/index.js +2 -1
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.js +3 -3
  18. package/dist/playground/assets.d.ts +5 -0
  19. package/dist/playground/assets.js +8 -0
  20. package/dist/playground/clang.d.ts +5 -0
  21. package/dist/playground/clang.js +45 -2
  22. package/dist/playground/index.d.ts +5 -5
  23. package/dist/playground/index.js +45 -5
  24. package/dist/playground/options.d.ts +9 -0
  25. package/dist/playground/python.d.ts +1 -0
  26. package/dist/playground/python.js +12 -0
  27. package/dist/playground/rust.d.ts +32 -0
  28. package/dist/playground/rust.js +175 -0
  29. package/dist/playground/sandbox.d.ts +18 -4
  30. package/dist/playground/wasmRustVersion.d.ts +1 -0
  31. package/dist/playground/wasmRustVersion.js +1 -0
  32. package/dist/playground/worker/clang.js +12 -4
  33. package/dist/playground/worker/rust.d.ts +1 -0
  34. package/dist/playground/worker/rust.js +159 -0
  35. package/dist/terminal/Terminal.svelte +78 -25
  36. package/dist/terminal/Terminal.svelte.d.ts +2 -0
  37. package/dist/terminal/theme.js +2 -2
  38. package/dist/terminal/types.d.ts +3 -0
  39. package/package.json +15 -9
@@ -0,0 +1,159 @@
1
+ import { waitForBufferedStdin } from '../stdinBuffer';
2
+ self.document = {
3
+ querySelectorAll() {
4
+ return [];
5
+ }
6
+ };
7
+ let stdinBufferRust = null;
8
+ let compilerUrl = '';
9
+ let runtimeBaseUrl = '';
10
+ let loadedCompilerUrl = '';
11
+ let compilerPromise = null;
12
+ let compiledArtifact = null;
13
+ let compiledCacheKey = '';
14
+ async function loadCompiler(url) {
15
+ if (!url) {
16
+ throw new Error('Rust runtime is not configured. Set PUBLIC_WASM_RUST_COMPILER_URL or runtimeAssets.rust.compilerUrl.');
17
+ }
18
+ if (loadedCompilerUrl === url && compilerPromise) {
19
+ return await compilerPromise;
20
+ }
21
+ loadedCompilerUrl = url;
22
+ try {
23
+ runtimeBaseUrl = new URL('./runtime/', url).toString();
24
+ }
25
+ catch {
26
+ runtimeBaseUrl = url;
27
+ }
28
+ compiledArtifact = null;
29
+ compiledCacheKey = '';
30
+ compilerPromise = (async () => {
31
+ const module = await import(/* @vite-ignore */ url);
32
+ const factory = typeof module.createRustCompiler === 'function'
33
+ ? module.createRustCompiler
34
+ : typeof module.default === 'function'
35
+ ? module.default
36
+ : null;
37
+ if (!factory) {
38
+ throw new Error('wasm-rust module must export createRustCompiler or a default factory');
39
+ }
40
+ if (typeof module.executeBrowserRustArtifact !== 'function') {
41
+ throw new Error('wasm-rust module must export executeBrowserRustArtifact');
42
+ }
43
+ return {
44
+ compiler: await factory(),
45
+ executeBrowserRustArtifact: module.executeBrowserRustArtifact
46
+ };
47
+ })();
48
+ return await compilerPromise;
49
+ }
50
+ self.onmessage = async (event) => {
51
+ const { load, compilerUrl: nextCompilerUrl, buffer, code, prepare, args = [], targetTriple = 'wasm32-wasip1', log } = event.data;
52
+ try {
53
+ if (load) {
54
+ compilerUrl = nextCompilerUrl;
55
+ if (log) {
56
+ console.log(`[wasm-idle:rust-worker] load compilerUrl=${compilerUrl}`);
57
+ }
58
+ await loadCompiler(compilerUrl);
59
+ postMessage({ load: true });
60
+ return;
61
+ }
62
+ stdinBufferRust = new Int32Array(buffer);
63
+ const runtime = await loadCompiler(compilerUrl);
64
+ const compileCacheKey = `${targetTriple}\n${code}`;
65
+ if (!compiledArtifact || compiledCacheKey !== compileCacheKey) {
66
+ if (log) {
67
+ console.log(`[wasm-idle:rust-worker] compile start prepare=${String(prepare)} target=${targetTriple} bytes=${code.length}`);
68
+ }
69
+ const result = await runtime.compiler.compile({
70
+ code,
71
+ edition: '2024',
72
+ crateType: 'bin',
73
+ targetTriple,
74
+ prepare,
75
+ log,
76
+ onProgress(progress) {
77
+ postMessage({ progress });
78
+ }
79
+ });
80
+ if (log) {
81
+ console.log(`[wasm-idle:rust-worker] compile settled success=${String(result.success)} hasWasm=${String(Boolean(result.artifact?.wasm))} stdout=${String(Boolean(result.stdout))} stderr=${String(Boolean(result.stderr))}`);
82
+ }
83
+ for (const diagnostic of result.diagnostics || []) {
84
+ postMessage({ diagnostic });
85
+ }
86
+ if (result.stdout)
87
+ postMessage({ output: result.stdout });
88
+ if (!result.success) {
89
+ throw new Error(result.stderr ||
90
+ result.diagnostics?.map((diagnostic) => diagnostic.message).join('\n') ||
91
+ 'Rust compilation failed');
92
+ }
93
+ if (result.stderr)
94
+ postMessage({ output: result.stderr });
95
+ if (!result.artifact?.wasm) {
96
+ throw new Error('wasm-rust did not return a wasm artifact');
97
+ }
98
+ compiledArtifact = result.artifact;
99
+ compiledCacheKey = compileCacheKey;
100
+ if (log) {
101
+ console.log(`[wasm-idle:rust-worker] cached artifact target=${compiledArtifact.targetTriple} format=${compiledArtifact.format}`);
102
+ }
103
+ }
104
+ if (prepare) {
105
+ if (log) {
106
+ console.log('[wasm-idle:rust-worker] prepare complete');
107
+ }
108
+ postMessage({ results: true });
109
+ return;
110
+ }
111
+ if (log) {
112
+ console.log(`[wasm-idle:rust-worker] runtime start target=${compiledArtifact.targetTriple} format=${compiledArtifact.format}`);
113
+ }
114
+ const execution = await runtime.executeBrowserRustArtifact(compiledArtifact, runtimeBaseUrl, {
115
+ args,
116
+ env: {
117
+ USER: 'jungol'
118
+ },
119
+ stdin: () => {
120
+ const chunk = waitForBufferedStdin(stdinBufferRust, () => postMessage({ buffer: true }));
121
+ if (chunk == null) {
122
+ if (log) {
123
+ console.log('[wasm-idle:rust-stdin] fd_read(bytes=0, eof=true)');
124
+ }
125
+ return null;
126
+ }
127
+ if (log) {
128
+ console.log(`[wasm-idle:rust-stdin] fd_fill(bytes=${new TextEncoder().encode(chunk).byteLength}, text=${JSON.stringify(chunk)})`);
129
+ }
130
+ return chunk;
131
+ },
132
+ stdout: (output) => {
133
+ if (output) {
134
+ postMessage({ output });
135
+ }
136
+ },
137
+ stderr: (output) => {
138
+ if (output) {
139
+ postMessage({ output });
140
+ }
141
+ }
142
+ });
143
+ if (log) {
144
+ console.log(`[wasm-idle:rust-worker] wasi run complete exitCode=${String(execution.exitCode)}`);
145
+ }
146
+ if (execution.exitCode !== 0) {
147
+ throw new Error(execution.stderr
148
+ ? `Rust program exited with code ${execution.exitCode}\n${execution.stderr}`
149
+ : `Rust program exited with code ${execution.exitCode}`);
150
+ }
151
+ postMessage({ results: true });
152
+ }
153
+ catch (error) {
154
+ if (log) {
155
+ console.error('[wasm-idle:rust-worker] failed', error);
156
+ }
157
+ postMessage({ error: error?.message || String(error) });
158
+ }
159
+ };
@@ -6,7 +6,11 @@
6
6
  SandboxExecutionOptions
7
7
  } from '../playground/options';
8
8
  import type { PlaygroundRuntimeAssets } from '../playground/assets';
9
- import type { Sandbox } from '../playground/sandbox';
9
+ import type {
10
+ BoundSandbox,
11
+ PlaygroundBinding,
12
+ SandboxRuntimeAssets
13
+ } from '../playground/sandbox';
10
14
  import type { TerminalControl } from './types';
11
15
  import { Theme, registerAllPlugins } from './';
12
16
  import load from '../playground';
@@ -18,6 +22,7 @@
18
22
  dark?: boolean;
19
23
  path?: string;
20
24
  runtimeAssets?: string | PlaygroundRuntimeAssets;
25
+ playground?: PlaygroundBinding;
21
26
  font?: string;
22
27
 
23
28
  onload?: () => void;
@@ -33,6 +38,7 @@
33
38
  dark = false,
34
39
  path = '',
35
40
  runtimeAssets = undefined,
41
+ playground = undefined,
36
42
  font = "'D2 coding', monospace",
37
43
  onload,
38
44
  onfinish,
@@ -47,15 +53,24 @@
47
53
  clientWidth = $state(0),
48
54
  clientHeight = $state(0),
49
55
  term = $state<TerminalType>(),
56
+ debugOutput = $state(''),
50
57
  finish = true,
51
58
  input = '',
52
- sandbox: Sandbox,
59
+ sandbox: BoundSandbox,
53
60
  first = true,
54
61
  tc = 0,
55
62
  plugin = $state(),
56
63
  ll: string | null = null,
64
+ loadedRuntimeAssets = $state<SandboxRuntimeAssets | undefined>(undefined),
65
+ loadedPlayground = $state<PlaygroundBinding | undefined>(undefined),
57
66
  stopRequested = false;
58
67
 
68
+ function writeTerminalOutput(text: string) {
69
+ if (!text) return;
70
+ debugOutput += text;
71
+ term?.write(text);
72
+ }
73
+
59
74
  function phaseProgress(
60
75
  progress: { set?: (value: number) => void } | undefined,
61
76
  start: number,
@@ -82,32 +97,42 @@
82
97
  }
83
98
 
84
99
  async function initSandbox(language: string) {
100
+ const currentPlayground = playground;
101
+ const currentRuntimeAssets = currentPlayground?.runtimeAssets || runtimeAssets || path;
85
102
  let _tc = ++tc;
86
103
  await wait();
87
104
  if (sandbox) await sandbox.clear();
88
105
  input = '';
89
106
  finish = false;
90
- if (ll !== language) {
91
- sandbox = await load(language);
107
+ if (
108
+ ll !== language ||
109
+ loadedRuntimeAssets !== currentRuntimeAssets ||
110
+ loadedPlayground !== currentPlayground
111
+ ) {
112
+ sandbox = currentPlayground
113
+ ? await currentPlayground.load(language)
114
+ : await load(language, currentRuntimeAssets);
92
115
  await sandbox.clear();
93
116
  ll = language;
117
+ loadedRuntimeAssets = currentRuntimeAssets;
118
+ loadedPlayground = currentPlayground;
94
119
  }
95
120
  sandbox.image = onimage;
96
121
  sandbox.ondebug = ondebug;
97
122
  sandbox.oncompilerdiagnostic = oncompilediagnostic;
98
123
  sandbox.output = (output: string) =>
99
- _tc === tc && term && term.write(output.replaceAll('\n', '\r\n'));
124
+ _tc === tc && writeTerminalOutput(output.replaceAll('\n', '\r\n'));
100
125
  }
101
126
 
102
127
  function runSandbox<T>(pr: Promise<T>) {
103
128
  return pr
104
129
  .then((x) => {
105
- term?.write(`\r\nProcess finished after ${sandbox.elapse}ms\u001B[?25l`);
130
+ writeTerminalOutput(`\r\nProcess finished after ${sandbox.elapse}ms\u001B[?25l`);
106
131
  return x;
107
132
  })
108
133
  .catch((msg) => {
109
134
  if (stopRequested) return false;
110
- term?.write(`\r\n\x1B[1;3;31m${msg}\u001B[?25l`);
135
+ writeTerminalOutput(`\r\n\x1B[1;3;31m${msg}\u001B[?25l`);
111
136
  return false;
112
137
  })
113
138
  .finally(() => {
@@ -137,7 +162,7 @@
137
162
 
138
163
  function submitCurrentInput() {
139
164
  term?.write('\r\n');
140
- sandbox.write?.(input + '\n');
165
+ sandbox?.write?.(input + '\n');
141
166
  input = '';
142
167
  }
143
168
 
@@ -149,12 +174,21 @@
149
174
  }
150
175
  }
151
176
 
177
+ async function waitForInput() {
178
+ await wait();
179
+ const startedAt = Date.now();
180
+ while (!sandbox && Date.now() - startedAt < 30_000) {
181
+ await new Promise((resolve) => setTimeout(resolve, 10));
182
+ }
183
+ }
184
+
152
185
  const terminalControl: TerminalControl = {
153
186
  async clear() {
154
187
  await wait();
155
188
  term?.reset();
156
189
  term?.write(`\u001B[?25l\x1b[0m\x1b[?25h`);
157
190
  if (term) term.options.cursorBlink = false;
191
+ debugOutput = '';
158
192
  input = '';
159
193
  first = true;
160
194
  await new Promise((r) => setTimeout(r, 100));
@@ -167,11 +201,16 @@
167
201
  args: string[] = [],
168
202
  options: SandboxExecutionOptions = {}
169
203
  ) {
170
- const loadProgress = phaseProgress(prog, 0, 0.85);
171
- const prepareProgress = phaseProgress(prog, 0.85, 0.99);
204
+ prog?.set?.(0);
205
+ const loadProgress =
206
+ language === 'RUST' ? phaseProgress(prog, 0, 0.05) : phaseProgress(prog, 0, 0.85);
207
+ const prepareProgress =
208
+ language === 'RUST'
209
+ ? phaseProgress(prog, 0.05, 0.99)
210
+ : phaseProgress(prog, 0.85, 0.99);
172
211
  await Promise.all([
173
212
  initSandbox(language).then(() =>
174
- sandbox.load(runtimeAssets || path, code, log, args, options, loadProgress)
213
+ sandbox.load(code, log, args, options, loadProgress)
175
214
  ),
176
215
  initTerm(false)
177
216
  ]);
@@ -189,9 +228,7 @@
189
228
  options: SandboxExecutionOptions = {}
190
229
  ) {
191
230
  await Promise.all([
192
- initSandbox(language).then(() =>
193
- sandbox.load(runtimeAssets || path, code, log, args, options, prog)
194
- ),
231
+ initSandbox(language).then(() => sandbox.load(code, log, args, options, prog)),
195
232
  initTerm()
196
233
  ]);
197
234
  return await runSandbox(sandbox.run(code, false, log, prog, args, options));
@@ -212,14 +249,25 @@
212
249
  await wait();
213
250
  sandbox.debugCommand?.(command);
214
251
  },
252
+ async setBreakpoints(lines: number[]) {
253
+ await wait();
254
+ sandbox.setBreakpoints?.(lines);
255
+ },
215
256
  async debugEvaluate(expression: string) {
216
257
  await wait();
217
258
  return (await sandbox.debugEvaluate?.(expression)) || '?';
218
259
  },
260
+ async waitForInput() {
261
+ await waitForInput();
262
+ },
219
263
  async write(input: string) {
220
- await wait();
264
+ await waitForInput();
221
265
  applyPastedText(input);
222
266
  if (!input.endsWith('\n') && !input.endsWith('\r')) submitCurrentInput();
267
+ },
268
+ async eof() {
269
+ await waitForInput();
270
+ sandbox?.eof?.();
223
271
  }
224
272
  };
225
273
 
@@ -251,9 +299,19 @@
251
299
  });
252
300
  term.open(ref);
253
301
  term.onKey((e: { key: string; domEvent: KeyboardEvent }) => {
254
- if (finish || !term) return;
302
+ if (!term) return;
255
303
  const ev = e.domEvent;
256
304
  const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
305
+ const isCopyShortcut = (ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'c';
306
+ if (isCopyShortcut && term.hasSelection()) {
307
+ const selectedText = term.getSelection();
308
+ if (selectedText) {
309
+ ev.preventDefault();
310
+ navigator.clipboard.writeText(selectedText).catch(() => {});
311
+ return;
312
+ }
313
+ }
314
+ if (finish) return;
257
315
  onkey?.(ev);
258
316
  if (ev.key === 'Enter') {
259
317
  submitCurrentInput();
@@ -270,18 +328,12 @@
270
328
  ) {
271
329
  appendInputText(e.key);
272
330
  }
273
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'c') {
274
- if (term.hasSelection()) {
275
- const selectedText = term.getSelection();
276
- if (selectedText) {
277
- navigator.clipboard.writeText(selectedText).catch(() => {});
278
- return;
279
- }
280
- }
331
+ } else if (isCopyShortcut) {
332
+ ev.preventDefault();
281
333
  sandbox.kill?.();
282
334
  } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'd') {
283
335
  if (input.length > 0) submitCurrentInput();
284
- else sandbox.eof?.();
336
+ sandbox?.eof?.();
285
337
  } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'v') {
286
338
  navigator.clipboard.readText().then((text) => {
287
339
  applyPastedText(text);
@@ -302,6 +354,7 @@
302
354
 
303
355
  <main>
304
356
  <div bind:this={ref} bind:clientWidth bind:clientHeight></div>
357
+ <pre data-testid="terminal-debug-output" style="display: none;">{debugOutput}</pre>
305
358
  </main>
306
359
 
307
360
  <style>
@@ -1,11 +1,13 @@
1
1
  import type { CompilerDiagnostic, DebugSessionEvent } from '../playground/options';
2
2
  import type { PlaygroundRuntimeAssets } from '../playground/assets';
3
+ import type { PlaygroundBinding } from '../playground/sandbox';
3
4
  import type { TerminalControl } from './types';
4
5
  import '@xterm/xterm/css/xterm.css';
5
6
  interface Props {
6
7
  dark?: boolean;
7
8
  path?: string;
8
9
  runtimeAssets?: string | PlaygroundRuntimeAssets;
10
+ playground?: PlaygroundBinding;
9
11
  font?: string;
10
12
  onload?: () => void;
11
13
  onfinish?: () => void;
@@ -1,7 +1,7 @@
1
1
  const theme = {
2
2
  Tango_Dark: {
3
3
  foreground: '#eee',
4
- background: 'transparent',
4
+ background: 'rgba(0, 0, 0, 0)',
5
5
  cursor: '#FFFFFF',
6
6
  cursorAccent: '#ffffff',
7
7
  selection: '#ffffff44',
@@ -24,7 +24,7 @@ const theme = {
24
24
  },
25
25
  Tango_Light: {
26
26
  foreground: '#111',
27
- background: 'rgba(0, 0, 0, 0)',
27
+ background: 'rgba(255, 255, 255, 0)',
28
28
  cursor: '#000000',
29
29
  cursorAccent: '#000000',
30
30
  selection: '#00000044',
@@ -9,7 +9,10 @@ export interface TerminalControl {
9
9
  destroy: () => Promise<void>;
10
10
  stop?: () => Promise<void>;
11
11
  debugCommand: (command: DebugCommand) => Promise<void>;
12
+ setBreakpoints?: (lines: number[]) => Promise<void>;
12
13
  debugEvaluate?: (expression: string) => Promise<string>;
14
+ waitForInput?: () => Promise<void>;
13
15
  write: (input: string) => Promise<void>;
16
+ eof?: () => Promise<void>;
14
17
  }
15
18
  export type { CompilerDiagnostic, DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, SandboxExecutionOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasm-idle",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "A simple web app to run C++/Python/Java code in the browser",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,9 @@
8
8
  "dev": "vite dev",
9
9
  "build": "vite build && npm run package",
10
10
  "preview": "vite preview",
11
+ "probe:rust-browser": "pnpm run sync:wasm-rust && pnpm build && node scripts/probe-rust-browser.mjs",
12
+ "test:browser:playwright": "pnpm run sync:wasm-rust && pnpm build && WASM_IDLE_RUN_REAL_BROWSER_RUST=1 pnpm exec vitest run src/lib/playground/rust.playwright.test.ts",
13
+ "sync:wasm-rust": "node scripts/sync-wasm-rust.mjs",
11
14
  "package": "svelte-kit sync && svelte-package && publint",
12
15
  "prepublishOnly": "npm run package",
13
16
  "test": "vitest run",
@@ -27,27 +30,30 @@
27
30
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
28
31
  "@testing-library/svelte": "^5.3.1",
29
32
  "@testing-library/user-event": "^14.6.1",
30
- "@typescript-eslint/eslint-plugin": "^8.57.1",
31
- "@typescript-eslint/parser": "^8.57.1",
32
- "eslint": "^10.0.3",
33
+ "@types/node": "^25.5.0",
34
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
35
+ "@typescript-eslint/parser": "^8.57.2",
36
+ "eslint": "^10.1.0",
33
37
  "eslint-config-prettier": "^10.1.8",
34
- "eslint-plugin-svelte": "^3.15.2",
38
+ "eslint-plugin-svelte": "^3.16.0",
35
39
  "eslint-plugin-svelte3": "^4.0.0",
36
40
  "gh-pages": "^6.3.0",
37
41
  "globals": "^17.4.0",
38
- "jsdom": "^29.0.0",
42
+ "jsdom": "^29.0.1",
39
43
  "monaco-editor": "^0.55.1",
44
+ "playwright-core": "^1.58.2",
40
45
  "prettier": "^3.8.1",
41
46
  "prettier-plugin-svelte": "^3.5.1",
42
- "svelte": "^5.54.0",
47
+ "svelte": "^5.55.0",
43
48
  "svelte-check": "^4.4.5",
44
49
  "tslib": "^2.8.1",
45
50
  "typescript": "^5.9.3",
46
51
  "typescript-eslint": "^8.57.1",
47
- "vite": "^8.0.0",
48
- "vitest": "^4.1.0"
52
+ "vite": "^8.0.3",
53
+ "vitest": "^4.1.1"
49
54
  },
50
55
  "dependencies": {
56
+ "@bjorn3/browser_wasi_shim": "^0.4.2",
51
57
  "@hancomac/monaco-languageclient": "^1.1.0",
52
58
  "@xterm/addon-canvas": "^0.7.0",
53
59
  "@xterm/addon-fit": "^0.11.0",