mrmd-js 2.0.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 (52) hide show
  1. package/README.md +842 -0
  2. package/dist/index.cjs +7613 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +7530 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/mrmd-js.iife.js +7618 -0
  7. package/dist/mrmd-js.iife.js.map +1 -0
  8. package/package.json +47 -0
  9. package/src/analysis/format.js +371 -0
  10. package/src/analysis/index.js +18 -0
  11. package/src/analysis/is-complete.js +394 -0
  12. package/src/constants.js +44 -0
  13. package/src/execute/css.js +205 -0
  14. package/src/execute/html.js +162 -0
  15. package/src/execute/index.js +41 -0
  16. package/src/execute/interface.js +144 -0
  17. package/src/execute/javascript.js +197 -0
  18. package/src/execute/registry.js +245 -0
  19. package/src/index.js +136 -0
  20. package/src/lsp/complete.js +353 -0
  21. package/src/lsp/format.js +310 -0
  22. package/src/lsp/hover.js +126 -0
  23. package/src/lsp/index.js +55 -0
  24. package/src/lsp/inspect.js +466 -0
  25. package/src/lsp/parse.js +455 -0
  26. package/src/lsp/variables.js +283 -0
  27. package/src/runtime.js +518 -0
  28. package/src/session/console-capture.js +181 -0
  29. package/src/session/context/iframe.js +407 -0
  30. package/src/session/context/index.js +12 -0
  31. package/src/session/context/interface.js +38 -0
  32. package/src/session/context/main.js +357 -0
  33. package/src/session/index.js +16 -0
  34. package/src/session/manager.js +327 -0
  35. package/src/session/session.js +678 -0
  36. package/src/transform/async.js +133 -0
  37. package/src/transform/extract.js +251 -0
  38. package/src/transform/index.js +10 -0
  39. package/src/transform/persistence.js +176 -0
  40. package/src/types/analysis.js +24 -0
  41. package/src/types/capabilities.js +44 -0
  42. package/src/types/completion.js +47 -0
  43. package/src/types/execution.js +62 -0
  44. package/src/types/index.js +16 -0
  45. package/src/types/inspection.js +39 -0
  46. package/src/types/session.js +32 -0
  47. package/src/types/streaming.js +74 -0
  48. package/src/types/variables.js +54 -0
  49. package/src/utils/ansi-renderer.js +301 -0
  50. package/src/utils/css-applicator.js +149 -0
  51. package/src/utils/html-renderer.js +355 -0
  52. package/src/utils/index.js +25 -0
package/src/runtime.js ADDED
@@ -0,0 +1,518 @@
1
+ /**
2
+ * MRP Runtime
3
+ *
4
+ * Main entry point for the mrmd-js runtime. Implements the MRMD Runtime
5
+ * Protocol (MRP) as a JavaScript API for browser-based execution.
6
+ *
7
+ * @module runtime
8
+ */
9
+
10
+ import { SessionManager } from './session/manager.js';
11
+ import { createDefaultExecutorRegistry } from './execute/index.js';
12
+ import { RUNTIME_NAME, RUNTIME_VERSION, DEFAULT_MAX_SESSIONS } from './constants.js';
13
+
14
+ /**
15
+ * @typedef {import('./types/capabilities.js').Capabilities} Capabilities
16
+ * @typedef {import('./types/capabilities.js').Features} Features
17
+ * @typedef {import('./types/capabilities.js').BrowserEnvironment} BrowserEnvironment
18
+ * @typedef {import('./types/session.js').SessionInfo} SessionInfo
19
+ * @typedef {import('./types/session.js').CreateSessionOptions} CreateSessionOptions
20
+ * @typedef {import('./types/session.js').IsolationMode} IsolationMode
21
+ * @typedef {import('./types/execution.js').ExecuteOptions} ExecuteOptions
22
+ * @typedef {import('./types/execution.js').ExecutionResult} ExecutionResult
23
+ * @typedef {import('./types/streaming.js').StreamEvent} StreamEvent
24
+ * @typedef {import('./types/completion.js').CompleteOptions} CompleteOptions
25
+ * @typedef {import('./types/completion.js').CompletionResult} CompletionResult
26
+ * @typedef {import('./types/inspection.js').InspectOptions} InspectOptions
27
+ * @typedef {import('./types/inspection.js').InspectResult} InspectResult
28
+ * @typedef {import('./types/inspection.js').HoverResult} HoverResult
29
+ * @typedef {import('./types/variables.js').VariableFilter} VariableFilter
30
+ * @typedef {import('./types/variables.js').VariableInfo} VariableInfo
31
+ * @typedef {import('./types/variables.js').VariableDetailOptions} VariableDetailOptions
32
+ * @typedef {import('./types/variables.js').VariableDetail} VariableDetail
33
+ * @typedef {import('./types/analysis.js').IsCompleteResult} IsCompleteResult
34
+ * @typedef {import('./types/analysis.js').FormatResult} FormatResult
35
+ * @typedef {import('./session/session.js').Session} Session
36
+ * @typedef {import('./execute/registry.js').ExecutorRegistry} ExecutorRegistry
37
+ * @typedef {import('./execute/interface.js').Executor} Executor
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} MrpRuntimeOptions
42
+ * @property {number} [maxSessions] - Maximum concurrent sessions
43
+ * @property {IsolationMode} [defaultIsolation='iframe'] - Default isolation mode
44
+ * @property {boolean} [defaultAllowMainAccess=false] - Allow main window access by default
45
+ */
46
+
47
+ /**
48
+ * Asset stored in the runtime
49
+ * @typedef {Object} StoredAsset
50
+ * @property {string} url - Blob URL
51
+ * @property {string} mimeType - MIME type
52
+ * @property {string} assetType - Asset type category
53
+ * @property {number} size - Size in bytes
54
+ */
55
+
56
+ /**
57
+ * Main MRP runtime for browser JavaScript
58
+ *
59
+ * @example
60
+ * const runtime = new MrpRuntime();
61
+ * const session = runtime.createSession({ language: 'javascript' });
62
+ * const result = await session.execute('const x = 1 + 2; x');
63
+ * console.log(result.resultString); // "3"
64
+ */
65
+ export class MrpRuntime {
66
+ /** @type {SessionManager} */
67
+ #sessionManager;
68
+
69
+ /** @type {ExecutorRegistry} */
70
+ #executorRegistry;
71
+
72
+ /** @type {MrpRuntimeOptions} */
73
+ #options;
74
+
75
+ /** @type {Map<string, StoredAsset>} */
76
+ #assets = new Map();
77
+
78
+ /**
79
+ * Create a new MRP runtime
80
+ * @param {MrpRuntimeOptions} [options]
81
+ */
82
+ constructor(options = {}) {
83
+ this.#options = {
84
+ maxSessions: options.maxSessions ?? DEFAULT_MAX_SESSIONS,
85
+ defaultIsolation: options.defaultIsolation ?? 'iframe',
86
+ defaultAllowMainAccess: options.defaultAllowMainAccess ?? false,
87
+ };
88
+
89
+ this.#executorRegistry = createDefaultExecutorRegistry();
90
+ this.#sessionManager = new SessionManager({
91
+ maxSessions: this.#options.maxSessions,
92
+ });
93
+ }
94
+
95
+ // ============================================================================
96
+ // Capabilities
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Get runtime capabilities (MRP /capabilities)
101
+ * @returns {Capabilities}
102
+ */
103
+ getCapabilities() {
104
+ return {
105
+ runtime: RUNTIME_NAME,
106
+ version: RUNTIME_VERSION,
107
+ languages: this.#executorRegistry.languages(),
108
+ features: this.#getFeatures(),
109
+ defaultSession: 'default',
110
+ maxSessions: this.#options.maxSessions ?? DEFAULT_MAX_SESSIONS,
111
+ environment: this.#getEnvironment(),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Get feature flags
117
+ * @returns {Features}
118
+ */
119
+ #getFeatures() {
120
+ return {
121
+ execute: true,
122
+ executeStream: true,
123
+ interrupt: true,
124
+ complete: true,
125
+ inspect: true,
126
+ hover: true,
127
+ variables: true,
128
+ variableExpand: true,
129
+ reset: true,
130
+ isComplete: true,
131
+ format: true,
132
+ assets: true,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Get environment info
138
+ * @returns {BrowserEnvironment}
139
+ */
140
+ #getEnvironment() {
141
+ return {
142
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
143
+ language: typeof navigator !== 'undefined' ? navigator.language : 'en',
144
+ platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
145
+ isSecureContext: typeof window !== 'undefined' ? window.isSecureContext : false,
146
+ };
147
+ }
148
+
149
+ // ============================================================================
150
+ // Sessions
151
+ // ============================================================================
152
+
153
+ /**
154
+ * List all active sessions (MRP GET /sessions)
155
+ * @returns {SessionInfo[]}
156
+ */
157
+ listSessions() {
158
+ return this.#sessionManager.list();
159
+ }
160
+
161
+ /**
162
+ * Create a new session (MRP POST /sessions)
163
+ * @param {CreateSessionOptions} [options]
164
+ * @returns {Session}
165
+ */
166
+ createSession(options = {}) {
167
+ const session = this.#sessionManager.create({
168
+ ...options,
169
+ isolation: options.isolation ?? this.#options.defaultIsolation,
170
+ allowMainAccess: options.allowMainAccess ?? this.#options.defaultAllowMainAccess,
171
+ executorRegistry: this.#executorRegistry,
172
+ });
173
+ return session;
174
+ }
175
+
176
+ /**
177
+ * Get a session by ID (MRP GET /sessions/{id})
178
+ * @param {string} id
179
+ * @returns {Session | undefined}
180
+ */
181
+ getSession(id) {
182
+ return this.#sessionManager.get(id);
183
+ }
184
+
185
+ /**
186
+ * Get or create a session
187
+ * @param {string} id
188
+ * @param {CreateSessionOptions} [options]
189
+ * @returns {Session}
190
+ */
191
+ getOrCreateSession(id, options = {}) {
192
+ const existing = this.#sessionManager.get(id);
193
+ if (existing) return existing;
194
+
195
+ return this.createSession({ ...options, id });
196
+ }
197
+
198
+ /**
199
+ * Destroy a session (MRP DELETE /sessions/{id})
200
+ * @param {string} id
201
+ * @returns {boolean}
202
+ */
203
+ destroySession(id) {
204
+ return this.#sessionManager.destroy(id);
205
+ }
206
+
207
+ /**
208
+ * Reset a session (MRP POST /sessions/{id}/reset)
209
+ * @param {string} id
210
+ * @returns {boolean}
211
+ */
212
+ resetSession(id) {
213
+ const session = this.#sessionManager.get(id);
214
+ if (!session) return false;
215
+ session.reset();
216
+ return true;
217
+ }
218
+
219
+ // ============================================================================
220
+ // Execution (convenience methods using default session)
221
+ // ============================================================================
222
+
223
+ /**
224
+ * Execute code (MRP POST /execute)
225
+ * @param {string} code
226
+ * @param {ExecuteOptions} [options]
227
+ * @returns {Promise<ExecutionResult>}
228
+ */
229
+ async execute(code, options = {}) {
230
+ const session = this.getOrCreateSession(options.session ?? 'default');
231
+ return session.execute(code, options);
232
+ }
233
+
234
+ /**
235
+ * Execute code with streaming output (MRP POST /execute/stream)
236
+ * @param {string} code
237
+ * @param {ExecuteOptions} [options]
238
+ * @returns {AsyncGenerator<StreamEvent>}
239
+ */
240
+ async *executeStream(code, options = {}) {
241
+ const session = this.getOrCreateSession(options.session ?? 'default');
242
+ yield* session.executeStream(code, options);
243
+ }
244
+
245
+ /**
246
+ * Send input to a waiting execution (MRP POST /input)
247
+ * @param {string} sessionId
248
+ * @param {string} execId
249
+ * @param {string} text
250
+ * @returns {boolean}
251
+ */
252
+ sendInput(sessionId, execId, text) {
253
+ const session = this.#sessionManager.get(sessionId);
254
+ if (!session) return false;
255
+ return session.sendInput(execId, text);
256
+ }
257
+
258
+ /**
259
+ * Interrupt execution (MRP POST /interrupt)
260
+ * @param {string} [sessionId]
261
+ * @param {string} [execId]
262
+ * @returns {boolean}
263
+ */
264
+ interrupt(sessionId, execId) {
265
+ if (sessionId) {
266
+ const session = this.#sessionManager.get(sessionId);
267
+ if (!session) return false;
268
+ return session.interrupt(execId);
269
+ }
270
+
271
+ // Interrupt all sessions
272
+ return this.#sessionManager.interruptAll();
273
+ }
274
+
275
+ // ============================================================================
276
+ // LSP Features (convenience methods)
277
+ // ============================================================================
278
+
279
+ /**
280
+ * Get completions (MRP POST /complete)
281
+ * @param {string} code
282
+ * @param {number} cursor
283
+ * @param {CompleteOptions} [options]
284
+ * @returns {CompletionResult}
285
+ */
286
+ complete(code, cursor, options = {}) {
287
+ const session = this.getOrCreateSession(options.session ?? 'default');
288
+ return session.complete(code, cursor, options);
289
+ }
290
+
291
+ /**
292
+ * Get hover info (MRP POST /hover)
293
+ * @param {string} code
294
+ * @param {number} cursor
295
+ * @param {string} [sessionId='default']
296
+ * @returns {HoverResult}
297
+ */
298
+ hover(code, cursor, sessionId = 'default') {
299
+ const session = this.getOrCreateSession(sessionId);
300
+ return session.hover(code, cursor);
301
+ }
302
+
303
+ /**
304
+ * Get inspection info (MRP POST /inspect)
305
+ * @param {string} code
306
+ * @param {number} cursor
307
+ * @param {InspectOptions} [options]
308
+ * @returns {InspectResult}
309
+ */
310
+ inspect(code, cursor, options = {}) {
311
+ const session = this.getOrCreateSession(options.session ?? 'default');
312
+ return session.inspect(code, cursor, options);
313
+ }
314
+
315
+ /**
316
+ * List variables (MRP POST /variables)
317
+ * @param {VariableFilter} [filter]
318
+ * @param {string} [sessionId='default']
319
+ * @returns {VariableInfo[]}
320
+ */
321
+ listVariables(filter, sessionId = 'default') {
322
+ const session = this.getOrCreateSession(sessionId);
323
+ return session.listVariables(filter);
324
+ }
325
+
326
+ /**
327
+ * Get variable detail (MRP POST /variables/{name})
328
+ * @param {string} name
329
+ * @param {VariableDetailOptions} [options]
330
+ * @returns {VariableDetail | null}
331
+ */
332
+ getVariable(name, options = {}) {
333
+ const session = this.getOrCreateSession(options.session ?? 'default');
334
+ return session.getVariable(name, options);
335
+ }
336
+
337
+ // ============================================================================
338
+ // Analysis
339
+ // ============================================================================
340
+
341
+ /**
342
+ * Check if code is complete (MRP POST /is_complete)
343
+ * @param {string} code
344
+ * @param {string} [sessionId='default']
345
+ * @returns {IsCompleteResult}
346
+ */
347
+ isComplete(code, sessionId = 'default') {
348
+ const session = this.getOrCreateSession(sessionId);
349
+ return session.isComplete(code);
350
+ }
351
+
352
+ /**
353
+ * Format code (MRP POST /format)
354
+ * @param {string} code
355
+ * @param {string} [sessionId='default']
356
+ * @returns {Promise<FormatResult>}
357
+ */
358
+ async format(code, sessionId = 'default') {
359
+ const session = this.getOrCreateSession(sessionId);
360
+ return session.format(code);
361
+ }
362
+
363
+ // ============================================================================
364
+ // Assets
365
+ // ============================================================================
366
+
367
+ /**
368
+ * Create an asset (blob URL)
369
+ * @param {Blob | string} content
370
+ * @param {string} mimeType
371
+ * @param {string} [name]
372
+ * @returns {import('./types/execution.js').Asset}
373
+ */
374
+ createAsset(content, mimeType, name) {
375
+ const blob = typeof content === 'string'
376
+ ? new Blob([content], { type: mimeType })
377
+ : content;
378
+
379
+ const url = URL.createObjectURL(blob);
380
+ const path = name ?? `asset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
381
+
382
+ const assetType = mimeType.startsWith('image/')
383
+ ? 'image'
384
+ : mimeType === 'text/html'
385
+ ? 'html'
386
+ : mimeType === 'application/json'
387
+ ? 'json'
388
+ : 'other';
389
+
390
+ this.#assets.set(path, {
391
+ url,
392
+ mimeType,
393
+ assetType,
394
+ size: blob.size,
395
+ });
396
+
397
+ return {
398
+ path,
399
+ url,
400
+ mimeType,
401
+ assetType,
402
+ size: blob.size,
403
+ };
404
+ }
405
+
406
+ /**
407
+ * Get an asset by path (MRP GET /assets/{path})
408
+ * @param {string} path
409
+ * @returns {string | null} Blob URL or null if not found
410
+ */
411
+ getAsset(path) {
412
+ const asset = this.#assets.get(path);
413
+ return asset?.url ?? null;
414
+ }
415
+
416
+ /**
417
+ * Get asset info
418
+ * @param {string} path
419
+ * @returns {StoredAsset | null}
420
+ */
421
+ getAssetInfo(path) {
422
+ return this.#assets.get(path) ?? null;
423
+ }
424
+
425
+ /**
426
+ * List all assets
427
+ * @returns {Array<{ path: string } & StoredAsset>}
428
+ */
429
+ listAssets() {
430
+ return Array.from(this.#assets.entries()).map(([path, asset]) => ({
431
+ path,
432
+ ...asset,
433
+ }));
434
+ }
435
+
436
+ /**
437
+ * Remove an asset
438
+ * @param {string} path
439
+ * @returns {boolean}
440
+ */
441
+ removeAsset(path) {
442
+ const asset = this.#assets.get(path);
443
+ if (!asset) return false;
444
+
445
+ URL.revokeObjectURL(asset.url);
446
+ this.#assets.delete(path);
447
+ return true;
448
+ }
449
+
450
+ /**
451
+ * Clear all assets
452
+ */
453
+ clearAssets() {
454
+ for (const asset of this.#assets.values()) {
455
+ URL.revokeObjectURL(asset.url);
456
+ }
457
+ this.#assets.clear();
458
+ }
459
+
460
+ // ============================================================================
461
+ // Extensibility
462
+ // ============================================================================
463
+
464
+ /**
465
+ * Register a custom executor for a language
466
+ * @param {string} language
467
+ * @param {Executor} executor
468
+ */
469
+ registerExecutor(language, executor) {
470
+ this.#executorRegistry.register(language, executor);
471
+ }
472
+
473
+ /**
474
+ * Register a language alias
475
+ * @param {string} alias
476
+ * @param {string} language
477
+ */
478
+ registerLanguageAlias(alias, language) {
479
+ this.#executorRegistry.registerAlias(alias, language);
480
+ }
481
+
482
+ /**
483
+ * Get the executor registry
484
+ * @returns {ExecutorRegistry}
485
+ */
486
+ getExecutorRegistry() {
487
+ return this.#executorRegistry;
488
+ }
489
+
490
+ /**
491
+ * Get the session manager
492
+ * @returns {SessionManager}
493
+ */
494
+ getSessionManager() {
495
+ return this.#sessionManager;
496
+ }
497
+
498
+ // ============================================================================
499
+ // Lifecycle
500
+ // ============================================================================
501
+
502
+ /**
503
+ * Destroy the runtime and clean up all resources
504
+ */
505
+ destroy() {
506
+ this.#sessionManager.destroyAll();
507
+ this.clearAssets();
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Create a new MRP runtime
513
+ * @param {MrpRuntimeOptions} [options]
514
+ * @returns {MrpRuntime}
515
+ */
516
+ export function createRuntime(options) {
517
+ return new MrpRuntime(options);
518
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Console Capture
3
+ *
4
+ * Intercepts console methods to capture output during execution.
5
+ * @module session/console-capture
6
+ */
7
+
8
+ /**
9
+ * @typedef {import('./context/interface.js').LogEntry} LogEntry
10
+ */
11
+
12
+ /**
13
+ * Format arguments for logging
14
+ * @param {Array<*>} args
15
+ * @returns {string}
16
+ */
17
+ function formatArgs(args) {
18
+ return args
19
+ .map((arg) => {
20
+ if (arg === null) return 'null';
21
+ if (arg === undefined) return 'undefined';
22
+ if (typeof arg === 'object') {
23
+ try {
24
+ return JSON.stringify(arg, null, 2);
25
+ } catch {
26
+ return String(arg);
27
+ }
28
+ }
29
+ return String(arg);
30
+ })
31
+ .join(' ');
32
+ }
33
+
34
+ /**
35
+ * Create a console capture for a window context
36
+ */
37
+ export class ConsoleCapture {
38
+ /** @type {Window} */
39
+ #context;
40
+
41
+ /** @type {LogEntry[]} */
42
+ #queue = [];
43
+
44
+ /** @type {Partial<Console> | null} */
45
+ #originalConsole = null;
46
+
47
+ /** @type {boolean} */
48
+ #active = false;
49
+
50
+ /**
51
+ * @param {Window} context - The window context to capture console from
52
+ */
53
+ constructor(context) {
54
+ this.#context = context;
55
+ }
56
+
57
+ /**
58
+ * Start capturing console output
59
+ */
60
+ start() {
61
+ if (this.#active) return;
62
+
63
+ const console = this.#context.console;
64
+
65
+ // Save originals
66
+ this.#originalConsole = {
67
+ log: console.log.bind(console),
68
+ info: console.info.bind(console),
69
+ warn: console.warn.bind(console),
70
+ error: console.error.bind(console),
71
+ };
72
+
73
+ // Intercept methods
74
+ console.log = (...args) => {
75
+ this.#queue.push({ type: 'log', args, timestamp: Date.now() });
76
+ this.#originalConsole?.log?.(...args);
77
+ };
78
+
79
+ console.info = (...args) => {
80
+ this.#queue.push({ type: 'info', args, timestamp: Date.now() });
81
+ this.#originalConsole?.info?.(...args);
82
+ };
83
+
84
+ console.warn = (...args) => {
85
+ this.#queue.push({ type: 'warn', args, timestamp: Date.now() });
86
+ this.#originalConsole?.warn?.(...args);
87
+ };
88
+
89
+ console.error = (...args) => {
90
+ this.#queue.push({ type: 'error', args, timestamp: Date.now() });
91
+ this.#originalConsole?.error?.(...args);
92
+ };
93
+
94
+ this.#active = true;
95
+ }
96
+
97
+ /**
98
+ * Stop capturing and restore original console
99
+ */
100
+ stop() {
101
+ if (!this.#active || !this.#originalConsole) return;
102
+
103
+ const console = this.#context.console;
104
+
105
+ if (this.#originalConsole.log) {
106
+ console.log = this.#originalConsole.log;
107
+ }
108
+ if (this.#originalConsole.info) {
109
+ console.info = this.#originalConsole.info;
110
+ }
111
+ if (this.#originalConsole.warn) {
112
+ console.warn = this.#originalConsole.warn;
113
+ }
114
+ if (this.#originalConsole.error) {
115
+ console.error = this.#originalConsole.error;
116
+ }
117
+
118
+ this.#originalConsole = null;
119
+ this.#active = false;
120
+ }
121
+
122
+ /**
123
+ * Clear the log queue
124
+ */
125
+ clear() {
126
+ this.#queue = [];
127
+ }
128
+
129
+ /**
130
+ * Get captured logs and clear queue
131
+ * @returns {LogEntry[]}
132
+ */
133
+ flush() {
134
+ const logs = this.#queue;
135
+ this.#queue = [];
136
+ return logs;
137
+ }
138
+
139
+ /**
140
+ * Get captured logs without clearing
141
+ * @returns {LogEntry[]}
142
+ */
143
+ peek() {
144
+ return [...this.#queue];
145
+ }
146
+
147
+ /**
148
+ * Convert logs to stdout/stderr strings
149
+ * @param {LogEntry[]} logs
150
+ * @returns {{ stdout: string, stderr: string }}
151
+ */
152
+ static toOutput(logs) {
153
+ const stdout = [];
154
+ const stderr = [];
155
+
156
+ for (const log of logs) {
157
+ const formatted = formatArgs(log.args);
158
+ if (log.type === 'error') {
159
+ stderr.push(`Error: ${formatted}`);
160
+ } else if (log.type === 'warn') {
161
+ stderr.push(`Warning: ${formatted}`);
162
+ } else {
163
+ stdout.push(formatted);
164
+ }
165
+ }
166
+
167
+ return {
168
+ stdout: stdout.join('\n'),
169
+ stderr: stderr.join('\n'),
170
+ };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Create a console capture for a context
176
+ * @param {Window} context
177
+ * @returns {ConsoleCapture}
178
+ */
179
+ export function createConsoleCapture(context) {
180
+ return new ConsoleCapture(context);
181
+ }