import "@ungap/with-resolvers"; import { $$ } from "basic-devtools"; import { assign, create } from "./utils.js"; import { getDetails } from "./script-handler.js"; import { registry as defaultRegistry, prefixes, configs, } from "./interpreters.js"; import { getRuntimeID } from "./loader.js"; import { io } from "./interpreter/_utils.js"; import { addAllListeners } from "./listeners.js"; import { Hook } from "./worker/hooks.js"; export const CUSTOM_SELECTORS = []; /** * @typedef {Object} Runtime custom configuration * @prop {object} interpreter the bootstrapped interpreter * @prop {(url:string, options?: object) => Worker} XWorker an XWorker constructor that defaults to same interpreter on the Worker. * @prop {object} config a cloned config used to bootstrap the interpreter * @prop {(code:string) => any} run an utility to run code within the interpreter * @prop {(code:string) => Promise} runAsync an utility to run code asynchronously within the interpreter * @prop {(path:string, data:ArrayBuffer) => void} writeFile an utility to write a file in the virtual FS, if available */ const types = new Map(); const waitList = new Map(); // REQUIRES INTEGRATION TEST /* c8 ignore start */ /** * @param {Element} node any DOM element registered via define. */ export const handleCustomType = (node) => { for (const selector of CUSTOM_SELECTORS) { if (node.matches(selector)) { const type = types.get(selector); const { resolve } = waitList.get(type); const { options, known } = registry.get(type); if (!known.has(node)) { known.add(node); const { interpreter: runtime, version, config, env, onRuntimeReady, } = options; const name = getRuntimeID(runtime, version); const id = env || `${name}${config ? `|${config}` : ""}`; const { interpreter: engine, XWorker: Worker } = getDetails( runtime, id, name, version, config, ); engine.then((interpreter) => { const module = create(defaultRegistry.get(runtime)); const { onBeforeRun, onBeforeRunAsync, onAfterRun, onAfterRunAsync, } = options; const hooks = new Hook(options); const XWorker = function XWorker(...args) { return Worker.apply(hooks, args); }; // These two loops mimic a `new Map(arrayContent)` without needing // the new Map overhead so that [name, [before, after]] can be easily destructured // and new sync or async patches become easy to add (when the logic is the same). // patch sync for (const [name, [before, after]] of [ ["run", [onBeforeRun, onAfterRun]], ]) { const method = module[name]; module[name] = function (interpreter, code) { if (before) before.call(this, resolved, node); const result = method.call(this, interpreter, code); if (after) after.call(this, resolved, node); return result; }; } // patch async for (const [name, [before, after]] of [ ["runAsync", [onBeforeRunAsync, onAfterRunAsync]], ]) { const method = module[name]; module[name] = async function (interpreter, code) { if (before) await before.call(this, resolved, node); const result = await method.call( this, interpreter, code, ); if (after) await after.call(this, resolved, node); return result; }; } module.setGlobal(interpreter, "XWorker", XWorker); const resolved = { type, interpreter, XWorker, io: io.get(interpreter), config: structuredClone(configs.get(name)), run: module.run.bind(module, interpreter), runAsync: module.runAsync.bind(module, interpreter), }; resolve(resolved); onRuntimeReady?.(resolved, node); }); } } } }; /** * @type {Map}>} */ const registry = new Map(); /** * @typedef {Object} PluginOptions custom configuration * @prop {'pyodide' | 'micropython' | 'wasmoon' | 'ruby-wasm-wasi'} interpreter the interpreter to use * @prop {string} [version] the optional interpreter version to use * @prop {string} [config] the optional config to use within such interpreter * @prop {(environment: object, node: Element) => void} [onRuntimeReady] the callback that will be invoked once */ /** * Allows custom types and components on the page to receive interpreters to execute any code * @param {string} type the unique `