@lincyaw/addon-exec 0.0.0-stage → 1.0.0-rc.10

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/LICENSE +202 -0
  2. package/LICENSES +696 -0
  3. package/README.md +33 -2
  4. package/components/PmRunPanel.css +167 -0
  5. package/components/PmRunPanel.tsx +389 -0
  6. package/components/PmStepRun.css +61 -0
  7. package/components/PmStepRun.tsx +202 -0
  8. package/package.json +50 -3
  9. package/src/client/RunOutputs.css +21 -0
  10. package/src/client/RunOutputs.tsx +69 -0
  11. package/src/client/StepMark.css +3 -0
  12. package/src/client/StepMark.tsx +12 -0
  13. package/src/client/env.d.ts +13 -0
  14. package/src/client/feedback.ts +69 -0
  15. package/src/client/format.ts +30 -0
  16. package/src/client/index.ts +14 -0
  17. package/src/client/messages.ts +102 -0
  18. package/src/client/outputs.ts +29 -0
  19. package/src/client/plugin.ts +55 -0
  20. package/src/client/pyodide/executor.ts +94 -0
  21. package/src/client/pyodide/presmium_gpu.py +297 -0
  22. package/src/client/pyodide/protocol.ts +27 -0
  23. package/src/client/pyodide/pyodide.worker.ts +68 -0
  24. package/src/client/pyodide/run_await.py +57 -0
  25. package/src/client/runs-in.ts +27 -0
  26. package/src/client/script/executor.ts +62 -0
  27. package/src/client/script/script.worker.ts +9 -0
  28. package/src/client/slide-fit.ts +61 -0
  29. package/src/client/step-marks.ts +7 -0
  30. package/src/client/when-service.ts +14 -0
  31. package/src/domain/browser-python.ts +33 -0
  32. package/src/domain/config.ts +41 -0
  33. package/src/domain/index.ts +11 -0
  34. package/src/domain/outputs.ts +18 -0
  35. package/src/domain/router.ts +50 -0
  36. package/src/domain/runs.ts +72 -0
  37. package/src/domain/static-output.ts +20 -0
  38. package/src/domain/trace.ts +32 -0
  39. package/src/node/browser-imports.ts +34 -0
  40. package/src/node/child-process.ts +109 -0
  41. package/src/node/code-component.ts +48 -0
  42. package/src/node/notebook-cells.ts +46 -0
  43. package/src/node/presmium.ts +27 -0
  44. package/src/node/pyodide.ts +22 -0
  45. package/src/node/python.ts +40 -0
  46. package/src/node/run-panel.ts +71 -0
  47. package/src/node/script-main.mjs +11 -0
  48. package/src/node/script.ts +26 -0
  49. package/src/node/step-run.ts +71 -0
  50. package/src/node/timeouts.ts +15 -0
  51. package/src/shared/script-runner.d.mts +16 -0
  52. package/src/shared/script-runner.mjs +168 -0
@@ -0,0 +1,46 @@
1
+ import { componentCode, jupyterKernelLanguage, languageId } from '@lincyaw/kernel'
2
+ import type { ComponentNode, IRBlock, MdastNode, RecordedRun, RendererContribution, RenderContext } from '@lincyaw/kernel'
3
+ import { paramsPrelude, shownOutput } from '../domain/index.ts'
4
+
5
+ function lines(text: string): string[] {
6
+ return text ? text.split(/(?<=\n)/) : []
7
+ }
8
+
9
+ function notebookOutputs(runs: RecordedRun[]): unknown[] {
10
+ return runs.flatMap<unknown>(run => run.outputs ? run.outputs.map((output) => {
11
+ if (output.output_type === 'stream') return { ...output, text: lines(output.text) }
12
+ if (output.output_type === 'error') return output
13
+ return { ...output, metadata: output.metadata ?? {}, ...output.output_type === 'execute_result' ? { execution_count: null } : {} }
14
+ }) : (['stdout', 'stderr'] as const).flatMap((name) => {
15
+ const text = run[name]
16
+ return text ? [{ output_type: 'stream', name, text: lines(text) }] : []
17
+ }))
18
+ }
19
+
20
+ function notebookCell(node: ComponentNode, _block: IRBlock, ctx: RenderContext) {
21
+ const source = componentCode(node)
22
+ if (!source) return undefined
23
+ const params = node.props.params as Record<string, unknown> | undefined
24
+ const runs = source.outputs ?? []
25
+ const recorded = typeof node.props.recorded === 'string' ? node.props.recorded : undefined
26
+ const output = shownOutput(runs, recorded)
27
+ if (languageId(source.lang) !== jupyterKernelLanguage(ctx.config.kernel)) {
28
+ const children: MdastNode[] = [{ type: 'code', lang: source.lang, value: paramsPrelude(source.lang, params) + source.value }]
29
+ const text = output.text.trimEnd()
30
+ if (text) children.push({ type: 'code', lang: 'text', value: text })
31
+ return { cell_type: 'markdown' as const, mdast: { type: 'root', children } }
32
+ }
33
+ const outputs = output.recorded ? [{ output_type: 'stream', name: 'stdout', text: lines(output.text) }] : notebookOutputs(runs)
34
+ const finished = runs.length > 0 && runs.every(run => !run.timedOut)
35
+ const metadata = node.props.timeout !== undefined && !finished ? { tags: ['skip-execution'] } : undefined
36
+ return { cell_type: 'code' as const, source: lines(paramsPrelude(source.lang, params) + source.value), outputs, execution_count: null, ...metadata ? { metadata } : {} }
37
+ }
38
+
39
+ export const rendererExtensions: Record<string, RendererContribution> = {
40
+ notebook: {
41
+ components: {
42
+ PmRunPanel: notebookCell,
43
+ PmStepRun: notebookCell,
44
+ },
45
+ },
46
+ }
@@ -0,0 +1,27 @@
1
+ import { createRequire } from 'node:module'
2
+ import type { PresmiumNodePlugin } from '@lincyaw/kernel'
3
+ import { EXEC_ID } from '../domain/index.ts'
4
+ import { browserImportsCheck } from './browser-imports.ts'
5
+ import { rendererExtensions } from './notebook-cells.ts'
6
+ import { pythonExecutor } from './python.ts'
7
+ import { runPanelDirective, runPanelFallbackPass, runPanelPass } from './run-panel.ts'
8
+ import { scriptExecutor } from './script.ts'
9
+ import { stepCountPass, stepRunDirective } from './step-run.ts'
10
+ import { timeoutPass } from './timeouts.ts'
11
+ import { pyodideAssets } from './pyodide.ts'
12
+
13
+ const { version } = createRequire(import.meta.url)('../../package.json') as { version: string }
14
+
15
+ const plugin: PresmiumNodePlugin = {
16
+ id: EXEC_ID,
17
+ version,
18
+ kernel: '^1.0.0',
19
+ directives: [runPanelDirective, stepRunDirective],
20
+ passes: [timeoutPass, runPanelPass, stepCountPass, runPanelFallbackPass],
21
+ checks: [browserImportsCheck],
22
+ executors: [pythonExecutor, scriptExecutor],
23
+ rendererExtensions,
24
+ assets: () => [pyodideAssets()],
25
+ }
26
+
27
+ export default plugin
@@ -0,0 +1,22 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { RuntimeAssets } from '@lincyaw/kernel'
5
+
6
+ const PYODIDE_DIR = 'pyodide/'
7
+ const LICENSES = 'LICENSES'
8
+ const LICENSES_FILE = fileURLToPath(new URL('../../LICENSES', import.meta.url))
9
+ const TYPES: Record<string, string> = {
10
+ 'pyodide.mjs': 'text/javascript',
11
+ 'pyodide.asm.mjs': 'text/javascript',
12
+ 'pyodide.asm.wasm': 'application/wasm',
13
+ 'python_stdlib.zip': 'application/zip',
14
+ 'pyodide-lock.json': 'application/json',
15
+ [LICENSES]: 'text/plain; charset=utf-8',
16
+ }
17
+
18
+ /** Pyodide's runtime, at `<base>pyodide/`. */
19
+ export function pyodideAssets(): RuntimeAssets {
20
+ const dir = dirname(createRequire(import.meta.url).resolve('pyodide/package.json'))
21
+ return { name: 'presmium:pyodide-runtime', prefix: PYODIDE_DIR, types: TYPES, source: file => (file === LICENSES ? LICENSES_FILE : join(dir, file)) }
22
+ }
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { readFileSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import type { Executor } from '@lincyaw/kernel'
6
+ import { isPython } from '../domain/index.ts'
7
+ import { createChildProcessRuns } from './child-process.ts'
8
+
9
+ const PYTHON_EXECUTOR_ID = 'presmium.python'
10
+
11
+ const PYTHON = 'python3'
12
+ const RUNNER = readFileSync(createRequire(import.meta.url).resolve('@lincyaw/kernel/runner.py'), 'utf8')
13
+ const RUNNER_REVISION = `runner ${createHash('sha256').update(RUNNER).digest('hex').slice(0, 16)}`
14
+
15
+ let probe: { version: string, prefixes: string[] } | undefined
16
+
17
+ function python(): { version: string, prefixes: string[] } {
18
+ if (!probe) {
19
+ const found = spawnSync(PYTHON, ['-c', 'import sys; print(sys.version.split()[0]); print(sys.prefix); print(sys.base_prefix)'], { encoding: 'utf8' })
20
+ const [version, ...prefixes] = found.status === 0 ? found.stdout.trim().split('\n') : []
21
+ probe = version ? { version: `Python ${version}`, prefixes: [...new Set(prefixes)] } : { version: '', prefixes: [] }
22
+ }
23
+ return probe
24
+ }
25
+
26
+ const runs = createChildProcessRuns({
27
+ executorId: PYTHON_EXECUTOR_ID,
28
+ argv: [PYTHON, '-c', `${RUNNER}\n_pm_main()\n`],
29
+ read: () => python().prefixes,
30
+ sessions: true,
31
+ })
32
+
33
+ export const pythonExecutor: Executor = {
34
+ id: PYTHON_EXECUTOR_ID,
35
+ supports: lang => isPython(lang) && python().version !== '',
36
+ fingerprint: () => python().version,
37
+ revision: () => RUNNER_REVISION,
38
+ run: runs.run,
39
+ endSession: runs.endSession,
40
+ }
@@ -0,0 +1,71 @@
1
+ import { componentCode, docBlocks } from '@lincyaw/kernel'
2
+ import type { DirectiveSpec, IRPass, MdastNode } from '@lincyaw/kernel'
3
+ import { EXEC_ID, paramsPrelude, shownOutput } from '../domain/index.ts'
4
+ import { codeComponent } from './code-component.ts'
5
+
6
+ export const runPanelDirective: DirectiveSpec = {
7
+ name: 'run-panel',
8
+ arg: true,
9
+ body: true,
10
+ options: { params: 'string', timeout: 'number', recorded: 'string', run: 'string' },
11
+ toIR(input, ctx) {
12
+ const when = input.options.run === undefined ? 'both' : String(input.options.run).trim()
13
+ if (when !== 'both' && when !== 'live') ctx.report({ checkId: 'presmium.exec.syntax', severity: 'warning', message: `{run-panel} :run: ${when} is not both or live; using both` })
14
+ const node = codeComponent('PmRunPanel', input, ctx, { when: when === 'live' ? 'live' : 'both' })
15
+ const recorded = input.options.recorded
16
+ if (typeof recorded === 'string') {
17
+ node.props.recorded = recorded
18
+ node.fallback?.children?.push({ type: 'code', lang: 'text', value: recorded })
19
+ }
20
+ return { node }
21
+ },
22
+ }
23
+
24
+ /** Gives each run panel its place in the document and its code's executor and session, which its runs send. */
25
+ export const runPanelPass: IRPass = {
26
+ name: 'presmium.exec.panels',
27
+ order: 10,
28
+ run(doc, ctx) {
29
+ for (const { section, block: { id, node, source, stepStates } } of docBlocks(doc)) {
30
+ if (node.type !== 'component' || node.plugin !== EXEC_ID || !['PmRunPanel', 'PmStepRun'].includes(node.name)) continue
31
+ const code = componentCode(node)
32
+ if (!code || node.children?.filter(child => child.node.type === 'code').length !== 1) {
33
+ ctx.report({ checkId: 'presmium.exec', severity: 'error', message: `${node.name} requires exactly one canonical code child.`, source })
34
+ continue
35
+ }
36
+ node.props.code = code.value
37
+ node.props.lang = code.lang
38
+ delete node.props.compiledOutput
39
+ for (const state of stepStates ?? []) if (state.parameters) {
40
+ for (const key of ['code', 'lang', 'place', 'executor', 'session', 'compiledOutput']) delete state.parameters[key]
41
+ }
42
+ node.props.place = { section: section.id, block: id }
43
+ const run = code.run
44
+ if (run?.executor) node.props.executor = run.executor
45
+ else delete node.props.executor
46
+ if (run?.session) node.props.session = run.session
47
+ else delete node.props.session
48
+ node.fallback = { type: 'root', children: [{ type: 'code', lang: code.lang, value: code.value }, ...typeof node.props.recorded === 'string' ? [{ type: 'code', lang: 'text', value: node.props.recorded }] : []] }
49
+ }
50
+ },
51
+ }
52
+
53
+ /** After the compile-time runs: the panel's fallback shows its code with the output, and a run that succeeded gives `compiledOutput`. */
54
+ export const runPanelFallbackPass: IRPass = {
55
+ name: 'presmium.exec.fallbacks',
56
+ after: 'execute',
57
+ run(doc) {
58
+ for (const { block: { node } } of docBlocks(doc)) {
59
+ if (node.type !== 'component' || node.plugin !== EXEC_ID || node.name !== 'PmRunPanel') continue
60
+ const code = componentCode(node)
61
+ if (!code) continue
62
+ const recorded = typeof node.props.recorded === 'string' ? node.props.recorded : undefined
63
+ const output = shownOutput(code.outputs ?? [], recorded).text.trimEnd()
64
+ const children: MdastNode[] = [{ type: 'code', lang: code.lang, value: paramsPrelude(code.lang, node.props.params as Record<string, unknown>) + code.value }]
65
+ if (output) children.push({ type: 'code', lang: 'text', value: output })
66
+ node.fallback = { type: 'root', children }
67
+ const runs = code.outputs ?? []
68
+ if (runs.length > 0 && runs.every(run => run.ok)) node.props.compiledOutput = shownOutput(runs, undefined).text
69
+ }
70
+ },
71
+ }
@@ -0,0 +1,11 @@
1
+ import { readFileSync, renameSync, writeFileSync } from 'node:fs'
2
+ import { runScript } from '../shared/script-runner.mjs'
3
+
4
+ const [requestFile, resultPrefix] = process.argv.slice(2)
5
+ const { runs } = JSON.parse(readFileSync(requestFile, 'utf8'))
6
+ for (const [i, run] of runs.entries()) {
7
+ const result = await runScript({ code: run.code, args: run.args, typescript: /^(ts|typescript)$/i.test(run.lang ?? '') })
8
+ writeFileSync(`${resultPrefix}${i}.part`, JSON.stringify(result))
9
+ renameSync(`${resultPrefix}${i}.part`, `${resultPrefix}${i}.json`)
10
+ }
11
+ process.exit(0)
@@ -0,0 +1,26 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { Executor } from '@lincyaw/kernel'
5
+ import { RUN_REQUEST_FILE, RUN_RESULT_PREFIX } from '@lincyaw/kernel/node'
6
+ import { isScript } from '../domain/index.ts'
7
+ import { createChildProcessRuns } from './child-process.ts'
8
+
9
+ export const NODE_SCRIPT_EXECUTOR_ID = 'presmium.node'
10
+
11
+ const MAIN = fileURLToPath(new URL('./script-main.mjs', import.meta.url))
12
+ const READ = [fileURLToPath(new URL('../..', import.meta.url)), dirname(createRequire(import.meta.url).resolve('sucrase/package.json')), dirname(dirname(process.execPath))]
13
+
14
+ const runs = createChildProcessRuns({
15
+ executorId: NODE_SCRIPT_EXECUTOR_ID,
16
+ argv: [process.execPath, MAIN, RUN_REQUEST_FILE, RUN_RESULT_PREFIX],
17
+ read: () => READ,
18
+ sessions: false,
19
+ })
20
+
21
+ export const scriptExecutor: Executor = {
22
+ id: NODE_SCRIPT_EXECUTOR_ID,
23
+ supports: isScript,
24
+ fingerprint: () => `node ${process.version}`,
25
+ run: runs.run,
26
+ }
@@ -0,0 +1,71 @@
1
+ import { componentCode, docBlocks, PROCESS_SANDBOX } from '@lincyaw/kernel'
2
+ import type { DirectiveSpec, IRPass, ProcessSandbox } from '@lincyaw/kernel'
3
+ import { minimalRunEnv } from '@lincyaw/kernel/node'
4
+ import { configuredTimeout, EXEC_ID, execConfig, isPython, stringArgs } from '../domain/index.ts'
5
+ import { codeComponent } from './code-component.ts'
6
+ import { pythonExecutor } from './python.ts'
7
+
8
+ export const stepRunDirective: DirectiveSpec = {
9
+ name: 'step-run',
10
+ arg: true,
11
+ body: true,
12
+ options: { steps: 'number', params: 'string', timeout: 'number' },
13
+ toIR(input, ctx) {
14
+ const node = codeComponent('PmStepRun', input, ctx)
15
+ const steps = Number(input.options.steps)
16
+ if (!Number.isInteger(steps) || steps < 1) {
17
+ ctx.report({ checkId: 'presmium.exec.syntax', severity: 'warning', message: '{step-run} needs :steps:, the number of lines the code executes; without it the slide has no steps' })
18
+ return { node }
19
+ }
20
+ node.props.steps = steps
21
+ return { node, span: steps }
22
+ },
23
+ }
24
+
25
+ const traced = new Map<string, Promise<number | undefined>>()
26
+
27
+ function traceLength(code: string, args: Record<string, string>, maxEvents: number, timeoutMs: number, sandbox: ProcessSandbox | undefined): Promise<number | undefined> {
28
+ const key = JSON.stringify([code, args, maxEvents])
29
+ let found = traced.get(key)
30
+ if (!found) {
31
+ const program = `import __main__\nprint(__main__._pm_trace(${JSON.stringify(code)}, ${JSON.stringify(JSON.stringify(args))}, '', '{}', ${maxEvents}))\n`
32
+ found = pythonExecutor.run({ lang: 'python', code: program, timeoutMs }, undefined, { env: minimalRunEnv(), ...sandbox ? { sandbox } : {} }).then((result) => {
33
+ if (!result.ok) return undefined
34
+ try {
35
+ const trace = JSON.parse(result.stdout) as { events: unknown[], truncated: boolean }
36
+ return trace.truncated ? undefined : trace.events.length
37
+ } catch {
38
+ return undefined
39
+ }
40
+ })
41
+ traced.set(key, found)
42
+ }
43
+ return found
44
+ }
45
+
46
+ /** Warns when a step run declares another number of steps than the lines its code executes. */
47
+ export const stepCountPass: IRPass = {
48
+ name: 'presmium.exec.steps',
49
+ order: 20,
50
+ async run(doc, ctx) {
51
+ if (!pythonExecutor.supports('python')) return
52
+ const { maxTraceEvents } = execConfig(ctx.config)
53
+ const sandbox = ctx.service?.(PROCESS_SANDBOX)
54
+ for (const { section, block } of docBlocks(doc)) {
55
+ const { node } = block
56
+ if (node.type !== 'component' || node.plugin !== EXEC_ID || node.name !== 'PmStepRun' || typeof node.props.steps !== 'number') continue
57
+ const code = componentCode(node)
58
+ if (!code || !isPython(code.lang)) continue
59
+ const timeoutMs = (typeof node.props.timeout === 'number' ? node.props.timeout : undefined) ?? configuredTimeout(ctx.configFor(section)) ?? execConfig(ctx.config).timeoutMs
60
+ const lines = await traceLength(code.value, stringArgs(node.props.params as Record<string, unknown> | undefined), maxTraceEvents, timeoutMs, sandbox)
61
+ if (lines === undefined || lines === node.props.steps) continue
62
+ ctx.report({
63
+ checkId: 'presmium.exec',
64
+ severity: 'warning',
65
+ message: `{step-run} declares :steps: ${node.props.steps}, but its code executes ${lines} lines: the slide's steps do not match the lines`,
66
+ hint: `write :steps: ${lines}`,
67
+ source: { file: block.source.file, line: block.source.line },
68
+ })
69
+ }
70
+ },
71
+ }
@@ -0,0 +1,15 @@
1
+ import { docBlocks } from '@lincyaw/kernel'
2
+ import type { IRPass } from '@lincyaw/kernel'
3
+ import { configuredTimeout } from '../domain/index.ts'
4
+
5
+ /** Code blocks without their own timeout take the one their section configures. */
6
+ export const timeoutPass: IRPass = {
7
+ name: 'presmium.exec.timeouts',
8
+ run(doc, ctx) {
9
+ for (const { section, block: { node } } of docBlocks(doc)) {
10
+ if (node.type !== 'code' || !node.run || node.run.timeoutMs !== undefined) continue
11
+ const timeout = configuredTimeout(ctx.configFor(section))
12
+ if (timeout) node.run.timeoutMs = timeout
13
+ }
14
+ },
15
+ }
@@ -0,0 +1,16 @@
1
+ export interface ScriptRequest {
2
+ code: string
3
+ args?: Record<string, string>
4
+ typescript?: boolean
5
+ imports?: Record<string, string>
6
+ }
7
+
8
+ export interface ScriptOutput {
9
+ stdout: string
10
+ stderr: string
11
+ exitCode: number
12
+ outputs: { output_type: 'stream', name: 'stdout' | 'stderr', text: string }[]
13
+ }
14
+
15
+ export function prepare(request: ScriptRequest): string
16
+ export function runScript(request: ScriptRequest): Promise<ScriptOutput>
@@ -0,0 +1,168 @@
1
+ import { transform } from 'sucrase'
2
+
3
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/
4
+
5
+ function show(value) {
6
+ if (typeof value === 'string') return value
7
+ if (value instanceof Error) return `${value.name}: ${value.message}`
8
+ if (typeof value === 'function') return `[Function ${value.name || '(anonymous)'}]`
9
+ if (typeof value === 'bigint') return `${value}n`
10
+ try {
11
+ return JSON.stringify(value) ?? String(value)
12
+ } catch {
13
+ return String(value)
14
+ }
15
+ }
16
+
17
+ function failure(error) {
18
+ const line = error instanceof Error ? /^.*data:text\/javascript.*?:(\d+):\d+\)?$/m.exec(error.stack ?? '')?.[1] : undefined
19
+ return `${show(error)}${line ? ` (line ${line})` : ''}`
20
+ }
21
+
22
+ function literal(raw) {
23
+ try {
24
+ JSON.parse(raw)
25
+ return raw
26
+ } catch {
27
+ return JSON.stringify(raw)
28
+ }
29
+ }
30
+
31
+ const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])([^'"\n]+)\2/g
32
+
33
+ export function prepare({ code, args = {}, typescript = false, imports = {} }) {
34
+ const js = typescript ? transform(code, { transforms: ['typescript'], disableESTransforms: true }).code : code
35
+ const constants = Object.entries(args).filter(([name]) => IDENTIFIER.test(name)).map(([name, raw]) => `const ${name} = ${literal(String(raw))};`)
36
+ const mapped = js.replace(SPECIFIER, (match, keyword, quote, name) => Object.hasOwn(imports, name) ? `${keyword}${quote}${imports[name]}${quote}` : match)
37
+ return constants.join(' ') + mapped
38
+ }
39
+
40
+ function trackTimers() {
41
+ const native = { setTimeout, clearTimeout, setInterval, clearInterval }
42
+ const { setTimeout: later, clearTimeout: cancel, setInterval: every } = native
43
+ const pending = new Set()
44
+ let armed = false
45
+ let failed
46
+ let settle
47
+ const done = new Promise((resolve, reject) => settle = { resolve, reject })
48
+ const decide = () => {
49
+ if (failed) settle.reject(failed.reason)
50
+ else if (pending.size === 0) settle.resolve()
51
+ }
52
+ const check = () => later(() => {
53
+ if (!armed) return
54
+ if (failed || pending.size > 0) return decide()
55
+ later(decide)
56
+ })
57
+ const fail = (reason) => {
58
+ failed ??= { reason }
59
+ check()
60
+ }
61
+ const call = (callback, args) => {
62
+ try {
63
+ const result = typeof callback === 'function' ? callback(...args) : undefined
64
+ if (typeof result?.then === 'function') result.then(undefined, fail)
65
+ } catch (error) {
66
+ fail(error)
67
+ }
68
+ }
69
+ const clear = (id) => {
70
+ pending.delete(id)
71
+ cancel(id)
72
+ check()
73
+ }
74
+ Object.assign(globalThis, {
75
+ setTimeout(callback, delay, ...args) {
76
+ const id = later(() => {
77
+ pending.delete(id)
78
+ call(callback, args)
79
+ check()
80
+ }, delay)
81
+ pending.add(id)
82
+ return id
83
+ },
84
+ setInterval(callback, delay, ...args) {
85
+ const id = every(() => call(callback, args), delay)
86
+ pending.add(id)
87
+ return id
88
+ },
89
+ clearTimeout: clear,
90
+ clearInterval: clear,
91
+ })
92
+ const node = typeof globalThis.process?.on === 'function' ? globalThis.process : undefined
93
+ const onNode = reason => fail(reason)
94
+ const onWorker = (event) => {
95
+ event.preventDefault()
96
+ fail(event.reason)
97
+ }
98
+ if (node) node.on('unhandledRejection', onNode)
99
+ else globalThis.addEventListener?.('unhandledrejection', onWorker)
100
+ return {
101
+ idle() {
102
+ armed = true
103
+ check()
104
+ return done
105
+ },
106
+ restore() {
107
+ for (const id of pending) cancel(id)
108
+ Object.assign(globalThis, native)
109
+ if (node) node.off('unhandledRejection', onNode)
110
+ else globalThis.removeEventListener?.('unhandledrejection', onWorker)
111
+ },
112
+ }
113
+ }
114
+
115
+ function collector() {
116
+ const text = { stdout: [], stderr: [] }
117
+ const outputs = []
118
+ return {
119
+ text,
120
+ outputs,
121
+ stream: name => ({
122
+ push(chunk) {
123
+ text[name].push(chunk)
124
+ const last = outputs.at(-1)
125
+ if (last?.name === name) last.text += chunk
126
+ else outputs.push({ output_type: 'stream', name, text: chunk })
127
+ },
128
+ }),
129
+ }
130
+ }
131
+
132
+ function captureStreams(out, err) {
133
+ const streams = [[globalThis.process?.stdout, out], [globalThis.process?.stderr, err]].filter(([stream]) => typeof stream?.write === 'function')
134
+ const saved = streams.map(([stream]) => [stream, stream.write])
135
+ for (const [stream, target] of streams) {
136
+ stream.write = (chunk, encoding, callback) => {
137
+ target.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk))
138
+ const done = typeof encoding === 'function' ? encoding : callback
139
+ if (typeof done === 'function') queueMicrotask(done)
140
+ return true
141
+ }
142
+ }
143
+ return () => saved.forEach(([stream, write]) => stream.write = write)
144
+ }
145
+
146
+ export async function runScript(request) {
147
+ const output = collector()
148
+ const out = output.stream('stdout')
149
+ const err = output.stream('stderr')
150
+ const write = target => (...values) => void target.push(`${values.map(show).join(' ')}\n`)
151
+ const saved = { log: console.log, info: console.info, debug: console.debug, dir: console.dir, warn: console.warn, error: console.error }
152
+ Object.assign(console, { log: write(out), info: write(out), debug: write(out), dir: value => write(out)(value), warn: write(err), error: write(err) })
153
+ const restoreStreams = captureStreams(out, err)
154
+ const timers = trackTimers()
155
+ let exitCode = 0
156
+ try {
157
+ await import(/* @vite-ignore */ `data:text/javascript;charset=utf-8,${encodeURIComponent(prepare(request))}`)
158
+ await timers.idle()
159
+ } catch (error) {
160
+ err.push(`${failure(error)}\n`)
161
+ exitCode = 1
162
+ } finally {
163
+ timers.restore()
164
+ restoreStreams()
165
+ Object.assign(console, saved)
166
+ }
167
+ return { stdout: output.text.stdout.join(''), stderr: output.text.stderr.join(''), exitCode, outputs: output.outputs }
168
+ }