wasm-idle 0.4.0 → 0.4.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.
@@ -1,4 +1,5 @@
1
1
  import type { CompilerDiagnostic, DebugCommand, DebugSessionEvent, SandboxExecutionOptions } from './options';
2
+ import type { PlaygroundRuntimeAssets } from './assets';
2
3
  import type { Writable } from 'svelte/store';
3
4
  type ProgressLike = Writable<number> | {
4
5
  set?: (value: number) => void;
@@ -6,7 +7,7 @@ type ProgressLike = Writable<number> | {
6
7
  export interface Sandbox {
7
8
  constructor: any;
8
9
  eof: () => void;
9
- load: (path: string, code?: string, log?: boolean, args?: string[], options?: SandboxExecutionOptions) => Promise<void>;
10
+ load: (runtimeAssets?: string | PlaygroundRuntimeAssets, code?: string, log?: boolean, args?: string[], options?: SandboxExecutionOptions, progress?: ProgressLike) => Promise<void>;
10
11
  run: (code: string, prepare: boolean, log?: boolean, prog?: ProgressLike, args?: string[], options?: SandboxExecutionOptions) => Promise<boolean | string>;
11
12
  terminate: () => void;
12
13
  clear: () => Promise<void>;
@@ -0,0 +1,12 @@
1
+ export interface WorkerRuntimeAssetConfig {
2
+ baseUrl: string;
3
+ useAssetBridge: boolean;
4
+ }
5
+ interface LoadedWorkerAsset {
6
+ bytes: Uint8Array;
7
+ mimeType?: string;
8
+ }
9
+ export declare function configureWorkerRuntimeAssets(config: WorkerRuntimeAssetConfig | null): void;
10
+ export declare function handleWorkerAssetMessage(data: any): boolean;
11
+ export declare function loadWorkerRuntimeAsset(asset: string): Promise<LoadedWorkerAsset>;
12
+ export {};
@@ -0,0 +1,257 @@
1
+ const decoder = new TextDecoder();
2
+ const originalFetch = globalThis.fetch.bind(globalThis);
3
+ const NativeXMLHttpRequest = globalThis.XMLHttpRequest;
4
+ let activeConfig = null;
5
+ let interceptorsInstalled = false;
6
+ let nextAssetRequestId = 0;
7
+ const pendingAssetRequests = new Map();
8
+ const responseBuffer = (bytes) => {
9
+ const buffer = bytes.buffer;
10
+ return bytes.byteOffset === 0 && bytes.byteLength === buffer.byteLength
11
+ ? buffer
12
+ : buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
13
+ };
14
+ const trackedAssetUrl = (input) => {
15
+ if (!activeConfig)
16
+ return null;
17
+ if (typeof input === 'string')
18
+ return new URL(input, activeConfig.baseUrl).href;
19
+ if (input instanceof URL)
20
+ return input.href;
21
+ return input.url;
22
+ };
23
+ const trackedAssetName = (url) => {
24
+ if (!activeConfig || !url.startsWith(activeConfig.baseUrl))
25
+ return null;
26
+ return url.slice(activeConfig.baseUrl.length);
27
+ };
28
+ const isTrackedAssetUrl = (url) => trackedAssetName(url) !== null;
29
+ const loadAssetFromBridge = async (asset) => {
30
+ const id = ++nextAssetRequestId;
31
+ return await new Promise((resolve, reject) => {
32
+ pendingAssetRequests.set(id, { resolve, reject });
33
+ self.postMessage({
34
+ assetRequest: {
35
+ id,
36
+ asset
37
+ }
38
+ });
39
+ });
40
+ };
41
+ const loadAssetFromUrl = async (url, asset) => {
42
+ const response = await originalFetch(url);
43
+ if (!response.ok)
44
+ throw new Error(`Failed to load ${asset}: ${response.status}`);
45
+ const total = Number(response.headers.get('content-length') || 0) || undefined;
46
+ const mimeType = response.headers.get('content-type') || undefined;
47
+ if (!response.body) {
48
+ const bytes = new Uint8Array(await response.arrayBuffer());
49
+ self.postMessage({
50
+ assetProgress: {
51
+ asset,
52
+ loaded: bytes.byteLength,
53
+ total: total ?? bytes.byteLength
54
+ }
55
+ });
56
+ return { bytes, mimeType };
57
+ }
58
+ const reader = response.body.getReader();
59
+ let receivedLength = 0;
60
+ const chunks = [];
61
+ while (true) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ if (!value)
66
+ continue;
67
+ const chunk = Uint8Array.from(value);
68
+ chunks.push(chunk);
69
+ receivedLength += chunk.byteLength;
70
+ self.postMessage({
71
+ assetProgress: {
72
+ asset,
73
+ loaded: receivedLength,
74
+ total
75
+ }
76
+ });
77
+ }
78
+ const bytes = new Uint8Array(receivedLength);
79
+ let position = 0;
80
+ for (const chunk of chunks) {
81
+ bytes.set(chunk, position);
82
+ position += chunk.byteLength;
83
+ }
84
+ self.postMessage({
85
+ assetProgress: {
86
+ asset,
87
+ loaded: receivedLength,
88
+ total: total ?? receivedLength
89
+ }
90
+ });
91
+ return { bytes, mimeType };
92
+ };
93
+ async function loadTrackedAsset(url) {
94
+ const asset = trackedAssetName(url);
95
+ if (!asset || !activeConfig)
96
+ throw new Error('Untracked runtime asset request');
97
+ return activeConfig.useAssetBridge
98
+ ? await loadAssetFromBridge(asset)
99
+ : await loadAssetFromUrl(url, asset);
100
+ }
101
+ function createTrackedResponse(asset) {
102
+ return new Response(responseBuffer(asset.bytes), {
103
+ status: 200,
104
+ headers: asset.mimeType ? { 'Content-Type': asset.mimeType } : undefined
105
+ });
106
+ }
107
+ function installTrackedFetch() {
108
+ if (typeof NativeXMLHttpRequest === 'undefined')
109
+ return;
110
+ class RuntimeAssetXMLHttpRequest {
111
+ responseType = '';
112
+ response = null;
113
+ responseText = '';
114
+ readyState = 0;
115
+ status = 0;
116
+ statusText = '';
117
+ timeout = 0;
118
+ withCredentials = false;
119
+ onload = null;
120
+ onerror = null;
121
+ onprogress = null;
122
+ onreadystatechange = null;
123
+ native = null;
124
+ url = '';
125
+ open(method, url) {
126
+ const resolvedUrl = trackedAssetUrl(url);
127
+ if (!resolvedUrl || !isTrackedAssetUrl(resolvedUrl)) {
128
+ const nativeUrl = resolvedUrl || (url instanceof URL ? url.href : String(url));
129
+ this.native = new NativeXMLHttpRequest();
130
+ this.native.responseType = this.responseType;
131
+ this.native.timeout = this.timeout;
132
+ this.native.withCredentials = this.withCredentials;
133
+ this.native.onload = (event) => {
134
+ this.response = this.native?.response;
135
+ this.responseText = this.native?.responseText || '';
136
+ this.readyState = this.native?.readyState || 0;
137
+ this.status = this.native?.status || 0;
138
+ this.statusText = this.native?.statusText || '';
139
+ this.onreadystatechange?.call(this, event);
140
+ this.onload?.call(this, event);
141
+ };
142
+ this.native.onerror = (event) => {
143
+ this.readyState = this.native?.readyState || 4;
144
+ this.status = this.native?.status || 0;
145
+ this.statusText = this.native?.statusText || '';
146
+ this.onreadystatechange?.call(this, event);
147
+ this.onerror?.call(this, event);
148
+ };
149
+ this.native.onprogress = (event) => {
150
+ this.onprogress?.call(this, event);
151
+ };
152
+ this.native.onreadystatechange = (event) => {
153
+ this.readyState = this.native?.readyState || 0;
154
+ this.onreadystatechange?.call(this, event);
155
+ };
156
+ this.native.open(method, nativeUrl);
157
+ return;
158
+ }
159
+ this.url = resolvedUrl;
160
+ this.readyState = 1;
161
+ this.onreadystatechange?.call(this, new ProgressEvent('readystatechange'));
162
+ }
163
+ setRequestHeader(name, value) {
164
+ this.native?.setRequestHeader(name, value);
165
+ }
166
+ async send(body) {
167
+ if (this.native) {
168
+ this.native.send(body);
169
+ return;
170
+ }
171
+ try {
172
+ const loaded = await loadTrackedAsset(this.url);
173
+ const buffer = responseBuffer(loaded.bytes);
174
+ this.status = 200;
175
+ this.statusText = 'OK';
176
+ this.readyState = 4;
177
+ if (this.responseType === 'arraybuffer') {
178
+ this.response = buffer;
179
+ }
180
+ else if (this.responseType === 'blob') {
181
+ this.response = new Blob([buffer], {
182
+ type: loaded.mimeType || 'application/octet-stream'
183
+ });
184
+ }
185
+ else {
186
+ const text = decoder.decode(loaded.bytes);
187
+ this.responseText = text;
188
+ this.response = text;
189
+ }
190
+ const progressEvent = new ProgressEvent('progress', {
191
+ lengthComputable: true,
192
+ loaded: loaded.bytes.byteLength,
193
+ total: loaded.bytes.byteLength
194
+ });
195
+ this.onprogress?.call(this, progressEvent);
196
+ this.onreadystatechange?.call(this, new ProgressEvent('readystatechange'));
197
+ this.onload?.call(this, new ProgressEvent('load'));
198
+ }
199
+ catch (error) {
200
+ this.readyState = 4;
201
+ this.status = 0;
202
+ this.statusText = error instanceof Error ? error.message : String(error);
203
+ this.onreadystatechange?.call(this, new ProgressEvent('readystatechange'));
204
+ this.onerror?.call(this, new ProgressEvent('error'));
205
+ }
206
+ }
207
+ abort() {
208
+ this.native?.abort();
209
+ }
210
+ getAllResponseHeaders() {
211
+ return this.native?.getAllResponseHeaders() || '';
212
+ }
213
+ getResponseHeader(name) {
214
+ return this.native?.getResponseHeader(name) || null;
215
+ }
216
+ }
217
+ globalThis.XMLHttpRequest = RuntimeAssetXMLHttpRequest;
218
+ }
219
+ function installRuntimeAssetInterceptors() {
220
+ if (interceptorsInstalled)
221
+ return;
222
+ interceptorsInstalled = true;
223
+ globalThis.fetch = (async (input, init) => {
224
+ const resolvedUrl = trackedAssetUrl(input);
225
+ if (!resolvedUrl || !isTrackedAssetUrl(resolvedUrl))
226
+ return originalFetch(input, init);
227
+ return createTrackedResponse(await loadTrackedAsset(resolvedUrl));
228
+ });
229
+ installTrackedFetch();
230
+ }
231
+ export function configureWorkerRuntimeAssets(config) {
232
+ activeConfig = config;
233
+ installRuntimeAssetInterceptors();
234
+ }
235
+ export function handleWorkerAssetMessage(data) {
236
+ const response = data?.assetResponse;
237
+ if (!response)
238
+ return false;
239
+ const pending = pendingAssetRequests.get(response.id);
240
+ if (!pending)
241
+ return true;
242
+ pendingAssetRequests.delete(response.id);
243
+ if (!response.ok) {
244
+ pending.reject(new Error(response.error || 'Runtime asset request failed'));
245
+ return true;
246
+ }
247
+ pending.resolve({
248
+ bytes: new Uint8Array(response.bytes),
249
+ mimeType: response.mimeType || undefined
250
+ });
251
+ return true;
252
+ }
253
+ export async function loadWorkerRuntimeAsset(asset) {
254
+ if (!activeConfig)
255
+ throw new Error('Runtime asset config unavailable');
256
+ return await loadTrackedAsset(new URL(asset, activeConfig.baseUrl).href);
257
+ }
@@ -17,7 +17,7 @@ async function loadClang(path, log) {
17
17
  });
18
18
  }
19
19
  self.onmessage = async (event) => {
20
- const { code, buffer, debugBuffer, load, interrupt, log, path, prepare, args, debug, breakpoints, pauseOnEntry } = event.data;
20
+ const { code, buffer, debugBuffer, load, interrupt, log, path, prepare, language, compileArgs, programArgs, cppVersion, cVersion, debug, breakpoints, pauseOnEntry } = event.data;
21
21
  if (load) {
22
22
  await loadClang(path, log);
23
23
  postMessage({ load: true });
@@ -28,7 +28,11 @@ self.onmessage = async (event) => {
28
28
  interruptBufferClang = new Uint8Array(interrupt);
29
29
  try {
30
30
  await clang.compileLink(code, {
31
- args,
31
+ language,
32
+ compileArgs,
33
+ programArgs,
34
+ cppVersion,
35
+ cVersion,
32
36
  debug,
33
37
  breakpoints,
34
38
  pauseOnEntry,
@@ -48,7 +52,11 @@ self.onmessage = async (event) => {
48
52
  interruptBufferClang = new Uint8Array(interrupt);
49
53
  try {
50
54
  await clang.compileLinkRun(code, {
51
- args,
55
+ language,
56
+ compileArgs,
57
+ programArgs,
58
+ cppVersion,
59
+ cVersion,
52
60
  debug,
53
61
  breakpoints,
54
62
  pauseOnEntry,
@@ -1,6 +1,6 @@
1
1
  import { prepareJavaStdinInjection } from '../javaStdin';
2
2
  import { waitForBufferedStdin } from '../stdinBuffer';
3
- import { resolveTeaVmAssetUrl } from '../teavmConfig';
3
+ import { configureWorkerRuntimeAssets, handleWorkerAssetMessage, loadWorkerRuntimeAsset } from './assets';
4
4
  let compilerLib = null;
5
5
  let compiler = null;
6
6
  let runtimeLoad = null;
@@ -15,18 +15,8 @@ let compiledStdin = '';
15
15
  let compiledMainClass = '';
16
16
  let compiledWasm = null;
17
17
  let currentSourcePath = '';
18
- const isGzip = (bytes) => bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
19
- const maybeDecompressWasm = async (bytes) => {
20
- if (!isGzip(bytes))
21
- return bytes;
22
- if (typeof DecompressionStream === 'undefined') {
23
- throw new Error('Generated WebAssembly is gzip-compressed, but DecompressionStream is unavailable');
24
- }
25
- const stream = new Blob([Uint8Array.from(bytes)])
26
- .stream()
27
- .pipeThrough(new DecompressionStream('gzip'));
28
- return new Uint8Array(await new Response(stream).arrayBuffer());
29
- };
18
+ const decoder = new TextDecoder();
19
+ const toInt8Array = (bytes) => new Int8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
30
20
  const flushStdout = () => {
31
21
  if (!stdoutBuffer)
32
22
  return;
@@ -50,41 +40,31 @@ const pushStderr = (charCode) => {
50
40
  flushStderr();
51
41
  };
52
42
  self.addEventListener('message', async (event) => {
53
- const { load, baseUrl, buffer, code, prepare, args = [], stdin = '' } = event.data;
43
+ if (handleWorkerAssetMessage(event.data))
44
+ return;
45
+ const { load, assets, buffer, code, prepare, args = [], stdin = '' } = event.data;
54
46
  try {
55
47
  if (load) {
48
+ const runtimeAssets = assets;
49
+ configureWorkerRuntimeAssets(runtimeAssets || null);
50
+ const baseUrl = runtimeAssets?.baseUrl || '';
56
51
  if (!compiler || loadedBaseUrl !== baseUrl) {
57
52
  loadedBaseUrl = baseUrl;
58
- const runtimeSource = await fetch(resolveTeaVmAssetUrl(baseUrl, 'compiler.wasm-runtime.js')).then((response) => {
59
- if (!response.ok)
60
- throw new Error(`Failed to load TeaVM compiler runtime: ${response.status}`);
61
- return response.text();
62
- });
53
+ const runtimeSource = decoder.decode((await loadWorkerRuntimeAsset('compiler.wasm-runtime.js')).bytes);
63
54
  const runtimeUrl = URL.createObjectURL(new Blob([runtimeSource], { type: 'text/javascript;charset=utf-8' }));
64
55
  const runtimeModule = await import(/* @vite-ignore */ runtimeUrl);
56
+ URL.revokeObjectURL(runtimeUrl);
65
57
  const runtimeLoadFn = runtimeModule.load;
66
58
  runtimeLoad = runtimeLoadFn;
67
- const compilerWasm = await fetch(resolveTeaVmAssetUrl(baseUrl, 'compiler.wasm')).then(async (response) => {
68
- if (!response.ok)
69
- throw new Error(`Failed to load TeaVM compiler wasm: ${response.status}`);
70
- return await maybeDecompressWasm(new Uint8Array(await response.arrayBuffer()));
71
- });
59
+ const compilerWasm = (await loadWorkerRuntimeAsset('compiler.wasm')).bytes;
72
60
  const compilerModule = await runtimeLoadFn(compilerWasm, {
73
61
  stackDeobfuscator: { enabled: false }
74
62
  });
75
63
  compilerLib = compilerModule.exports;
76
64
  compiler = compilerLib.createCompiler();
77
65
  const [sdk, runtimeClasslib] = await Promise.all([
78
- fetch(resolveTeaVmAssetUrl(baseUrl, 'compile-classlib-teavm.bin')).then(async (response) => {
79
- if (!response.ok)
80
- throw new Error(`Failed to load TeaVM SDK: ${response.status}`);
81
- return new Int8Array(await response.arrayBuffer());
82
- }),
83
- fetch(resolveTeaVmAssetUrl(baseUrl, 'runtime-classlib-teavm.bin')).then(async (response) => {
84
- if (!response.ok)
85
- throw new Error(`Failed to load TeaVM runtime classlib: ${response.status}`);
86
- return new Int8Array(await response.arrayBuffer());
87
- })
66
+ loadWorkerRuntimeAsset('compile-classlib-teavm.bin').then(({ bytes }) => toInt8Array(bytes)),
67
+ loadWorkerRuntimeAsset('runtime-classlib-teavm.bin').then(({ bytes }) => toInt8Array(bytes))
88
68
  ]);
89
69
  compiler.setSdk(sdk);
90
70
  compiler.setTeaVMClasslib(runtimeClasslib);
@@ -185,7 +165,7 @@ self.addEventListener('message', async (event) => {
185
165
  compiledCode = code;
186
166
  compiledStdin = stdinInjection.stdinCacheKey;
187
167
  compiledMainClass = mainClass;
188
- compiledWasm = await maybeDecompressWasm(new Uint8Array(compiler.getWebAssemblyOutputFile('app.wasm')));
168
+ compiledWasm = new Uint8Array(compiler.getWebAssemblyOutputFile('app.wasm'));
189
169
  }
190
170
  if (prepare) {
191
171
  self.postMessage({ results: true });
@@ -1,4 +1,5 @@
1
1
  import { flushQueuedStdin, readBufferedStdin, waitForBufferedStdin } from '../stdinBuffer';
2
+ import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
2
3
  self.document = {
3
4
  querySelector() {
4
5
  return null;
@@ -7,8 +8,7 @@ self.document = {
7
8
  return [];
8
9
  }
9
10
  };
10
- let stdinBufferPyodide, debugBufferPyodide, watchBufferPyodide, watchResultBufferPyodide, interruptBufferPyodide, pyodide, path = '', packageBaseUrl = '';
11
- let jungolRobotReady = false;
11
+ let stdinBufferPyodide, debugBufferPyodide, watchBufferPyodide, watchResultBufferPyodide, interruptBufferPyodide, pyodide, baseUrl = '', packageBaseUrl = '';
12
12
  const imageHook = `
13
13
  if not globals().get("__wasm_idle_img_inited__", False):
14
14
  globals()["__wasm_idle_img_inited__"] = True
@@ -157,33 +157,12 @@ async function loadPyodide(path) {
157
157
  if (pyodide)
158
158
  return;
159
159
  const { loadPyodide } = await import('pyodide');
160
- pyodide = await loadPyodide({ indexURL: path + '/pyodide' });
161
- packageBaseUrl = path + '/pyodide';
160
+ pyodide = await loadPyodide({ indexURL: path });
161
+ packageBaseUrl = path;
162
162
  const setCdnUrl = pyodide?.setCdnUrl;
163
163
  if (typeof setCdnUrl === 'function')
164
164
  setCdnUrl(packageBaseUrl);
165
165
  }
166
- async function ensureRobotJungol() {
167
- if (jungolRobotReady || !pyodide)
168
- return;
169
- try {
170
- const res = await fetch(path + '/jungol-robot/jungol_robot.zip');
171
- if (!res.ok)
172
- throw new Error('jungol-robot zip not found');
173
- const data = await res.arrayBuffer();
174
- const siteRaw = pyodide.runPython('import site; site.getsitepackages()[0]');
175
- const sitePath = typeof siteRaw === 'string' ? siteRaw : siteRaw.toString();
176
- const unpackArchive = pyodide?.unpackArchive;
177
- if (typeof unpackArchive !== 'function')
178
- throw new Error('pyodide.unpackArchive unavailable');
179
- unpackArchive(data, 'zip', sitePath);
180
- pyodide.runPython('import importlib; importlib.invalidate_caches()');
181
- jungolRobotReady = true;
182
- }
183
- catch (e) {
184
- console.warn('jungol-robot preload failed', e);
185
- }
186
- }
187
166
  async function loadPackages(code) {
188
167
  if (!code)
189
168
  return;
@@ -203,20 +182,22 @@ async function loadPackages(code) {
203
182
  }
204
183
  }
205
184
  self.onmessage = async (event) => {
206
- const { code, buffer, debugBuffer, watchBuffer, watchResultBuffer, load, interrupt, path: _p, prepare, debug = false, breakpoints = [], pauseOnEntry = false } = event.data;
185
+ if (handleWorkerAssetMessage(event.data))
186
+ return;
187
+ const { code, buffer, debugBuffer, watchBuffer, watchResultBuffer, load, interrupt, assets, prepare, debug = false, breakpoints = [], pauseOnEntry = false } = event.data;
207
188
  if (load) {
208
- path = _p;
189
+ const runtimeAssets = assets;
190
+ baseUrl = runtimeAssets?.baseUrl || baseUrl;
191
+ configureWorkerRuntimeAssets(runtimeAssets || null);
209
192
  postMessage({ output: 'Loading Pyodide...' });
210
- await loadPyodide(path);
211
- await ensureRobotJungol();
193
+ await loadPyodide(baseUrl);
212
194
  postMessage({ output: ' Done.\n\r' });
213
195
  postMessage({ load: true });
214
196
  }
215
197
  else if (prepare) {
216
198
  postMessage({ output: 'Loading packages...' });
217
199
  try {
218
- await loadPyodide(path);
219
- await ensureRobotJungol();
200
+ await loadPyodide(baseUrl);
220
201
  await loadPackages(code);
221
202
  postMessage({ output: ' Done.\n\r' });
222
203
  self.postMessage({ results: true });
@@ -226,8 +207,7 @@ self.onmessage = async (event) => {
226
207
  }
227
208
  }
228
209
  else if (code) {
229
- await loadPyodide(path);
230
- await ensureRobotJungol();
210
+ await loadPyodide(baseUrl);
231
211
  await loadPackages(code);
232
212
  const ts = Date.now();
233
213
  stdinBufferPyodide = new Int32Array(buffer);
@@ -5,6 +5,7 @@
5
5
  DebugSessionEvent,
6
6
  SandboxExecutionOptions
7
7
  } from '../playground/options';
8
+ import type { PlaygroundRuntimeAssets } from '../playground/assets';
8
9
  import type { Sandbox } from '../playground/sandbox';
9
10
  import type { TerminalControl } from './types';
10
11
  import { Theme, registerAllPlugins } from './';
@@ -16,6 +17,7 @@
16
17
  interface Props {
17
18
  dark?: boolean;
18
19
  path?: string;
20
+ runtimeAssets?: string | PlaygroundRuntimeAssets;
19
21
  font?: string;
20
22
 
21
23
  onload?: () => void;
@@ -30,6 +32,7 @@
30
32
  let {
31
33
  dark = false,
32
34
  path = '',
35
+ runtimeAssets = undefined,
33
36
  font = "'D2 coding', monospace",
34
37
  onload,
35
38
  onfinish,
@@ -50,7 +53,22 @@
50
53
  first = true,
51
54
  tc = 0,
52
55
  plugin = $state(),
53
- ll: string | null = null;
56
+ ll: string | null = null,
57
+ stopRequested = false;
58
+
59
+ function phaseProgress(
60
+ progress: { set?: (value: number) => void } | undefined,
61
+ start: number,
62
+ end: number
63
+ ) {
64
+ if (!progress) return undefined;
65
+ return {
66
+ set(value: number) {
67
+ const clamped = Number.isFinite(value) ? Math.min(Math.max(value, 0), 1) : 0;
68
+ progress.set?.(start + (end - start) * clamped);
69
+ }
70
+ };
71
+ }
54
72
 
55
73
  function wait() {
56
74
  return new Promise<void>((r) => {
@@ -88,10 +106,12 @@
88
106
  return x;
89
107
  })
90
108
  .catch((msg) => {
109
+ if (stopRequested) return false;
91
110
  term?.write(`\r\n\x1B[1;3;31m${msg}\u001B[?25l`);
92
111
  return false;
93
112
  })
94
113
  .finally(() => {
114
+ stopRequested = false;
95
115
  onfinish?.();
96
116
  finish = true;
97
117
  if (term) term.options.cursorBlink = false;
@@ -102,6 +122,7 @@
102
122
  await wait();
103
123
  if (!term) return;
104
124
  term.options.cursorBlink = blink;
125
+ term.write('\u001B[?25h');
105
126
  term.focus();
106
127
 
107
128
  if (!first) term?.write(`\r\n\x1b[0m`);
@@ -146,11 +167,18 @@
146
167
  args: string[] = [],
147
168
  options: SandboxExecutionOptions = {}
148
169
  ) {
170
+ const loadProgress = phaseProgress(prog, 0, 0.85);
171
+ const prepareProgress = phaseProgress(prog, 0.85, 0.99);
149
172
  await Promise.all([
150
- initSandbox(language).then(() => sandbox.load(path, code, log, args, options)),
173
+ initSandbox(language).then(() =>
174
+ sandbox.load(runtimeAssets || path, code, log, args, options, loadProgress)
175
+ ),
151
176
  initTerm(false)
152
177
  ]);
153
- return !!(await runSandbox(sandbox.run(code, true, log, prog, args, options)));
178
+ prepareProgress?.set?.(0);
179
+ return !!(await runSandbox(
180
+ sandbox.run(code, true, log, prepareProgress, args, options)
181
+ ));
154
182
  },
155
183
  async run(
156
184
  language: string,
@@ -161,7 +189,9 @@
161
189
  options: SandboxExecutionOptions = {}
162
190
  ) {
163
191
  await Promise.all([
164
- initSandbox(language).then(() => sandbox.load(path, code, log, args, options)),
192
+ initSandbox(language).then(() =>
193
+ sandbox.load(runtimeAssets || path, code, log, args, options, prog)
194
+ ),
165
195
  initTerm()
166
196
  ]);
167
197
  return await runSandbox(sandbox.run(code, false, log, prog, args, options));
@@ -171,6 +201,13 @@
171
201
  term?.dispose();
172
202
  if (sandbox) await sandbox.clear();
173
203
  },
204
+ async stop() {
205
+ await wait();
206
+ stopRequested = true;
207
+ finish = true;
208
+ if (sandbox?.kill) sandbox.kill();
209
+ else sandbox?.terminate?.();
210
+ },
174
211
  async debugCommand(command: DebugCommand) {
175
212
  await wait();
176
213
  sandbox.debugCommand?.(command);
@@ -233,12 +270,19 @@
233
270
  ) {
234
271
  appendInputText(e.key);
235
272
  }
236
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'c') {
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
+ }
237
281
  sandbox.kill?.();
238
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'd') {
282
+ } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'd') {
239
283
  if (input.length > 0) submitCurrentInput();
240
284
  else sandbox.eof?.();
241
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'v') {
285
+ } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'v') {
242
286
  navigator.clipboard.readText().then((text) => {
243
287
  applyPastedText(text);
244
288
  });
@@ -272,4 +316,10 @@
272
316
  width: 100%;
273
317
  height: 100%;
274
318
  }
319
+
320
+ :global(.xterm),
321
+ :global(.xterm .xterm-viewport),
322
+ :global(.xterm .composition-view) {
323
+ background-color: transparent;
324
+ }
275
325
  </style>
@@ -1,9 +1,11 @@
1
1
  import type { CompilerDiagnostic, DebugSessionEvent } from '../playground/options';
2
+ import type { PlaygroundRuntimeAssets } from '../playground/assets';
2
3
  import type { TerminalControl } from './types';
3
4
  import '@xterm/xterm/css/xterm.css';
4
5
  interface Props {
5
6
  dark?: boolean;
6
7
  path?: string;
8
+ runtimeAssets?: string | PlaygroundRuntimeAssets;
7
9
  font?: string;
8
10
  onload?: () => void;
9
11
  onfinish?: () => void;
@@ -1,7 +1,7 @@
1
1
  const theme = {
2
2
  Tango_Dark: {
3
3
  foreground: '#eee',
4
- background: '#111827',
4
+ background: 'transparent',
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: '#FFFFFF',
27
+ background: 'rgba(0, 0, 0, 0)',
28
28
  cursor: '#000000',
29
29
  cursorAccent: '#000000',
30
30
  selection: '#00000044',