'use strict'; require('@ungap/with-resolvers'); const { $$ } = require('basic-devtools'); const { JSModules, assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo, registerJSModules } = require('./utils.js'); const { getDetails } = require('./script-handler.js'); const { registry: defaultRegistry, prefixes, configs } = require('./interpreters.js'); const { getRuntimeID } = require('./loader.js'); const { addAllListeners } = require('./listeners.js'); const { Hook, XWorker: XW } = require('./xworker.js'); const { polluteJS, js: jsHooks, code: codeHooks } = require('./hooks.js'); const workerURL = (require('./worker/url.js')); const CUSTOM_SELECTORS = []; exports.CUSTOM_SELECTORS = CUSTOM_SELECTORS; const customObserver = new Map(); exports.customObserver = customObserver; /** * @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. */ const handleCustomType = async (node) => { for (const selector of CUSTOM_SELECTORS) { if (node.matches(selector)) { const type = types.get(selector); const details = registry.get(type); const { resolve } = waitList.get(type); const { options, known } = details; if (known.has(node)) return; known.add(node); for (const [selector, callback] of customObserver) { if (node.matches(selector)) await callback(node); } const { interpreter: runtime, configURL, config, version, env, onerror, hooks, } = options; let error; try { const worker = workerURL(node); if (worker) { const xworker = XW.call(new Hook(null, hooks), worker, { ...nodeInfo(node, type), version, configURL, type: runtime, custom: type, config: node.getAttribute('config') || config || {}, async: node.hasAttribute('async') }); defineProperty(node, 'xworker', { value: xworker }); resolve({ type, xworker }); return; } } // let the custom type handle errors via its `io` catch (workerError) { error = workerError; } const name = getRuntimeID(runtime, version); const id = env || `${name}${config ? `|${config}` : ''}`; const { interpreter: engine, XWorker: Worker } = getDetails( type, id, name, version, config, configURL, runtime ); const interpreter = await engine; const module = create(defaultRegistry.get(runtime)); const hook = new Hook(interpreter, hooks); const XWorker = function XWorker(...args) { return Worker.apply(hook, args); }; const resolved = { ...createResolved( module, type, structuredClone(configs.get(name)), interpreter, ), XWorker, }; registerJSModules(runtime, module, interpreter, JSModules); module.registerJSModule(interpreter, 'polyscript', { XWorker, config: resolved.config, currentScript: type.startsWith('_') ? null : node, js_modules: JSModules, }); // patch methods accordingly to hooks (and only if needed) for (const suffix of ['Run', 'RunAsync']) { let before = ''; let after = ''; for (const key of codeHooks) { const value = hooks?.main?.[key]; if (value && key.endsWith(suffix)) { if (key.startsWith('codeBefore')) before = dedent(value()); else after = dedent(value()); } } if (before || after) { createOverload( module, `r${suffix.slice(1)}`, before, after, ); } let beforeCB, afterCB; // ignore onReady and onWorker for (let i = 2; i < jsHooks.length; i++) { const key = jsHooks[i]; const value = hooks?.main?.[key]; if (value && key.endsWith(suffix)) { if (key.startsWith('onBefore')) beforeCB = value; else afterCB = value; } } polluteJS(module, resolved, node, suffix.endsWith('Async'), beforeCB, afterCB); } details.queue = details.queue.then(() => { resolve(resolved); if (error) onerror?.(error, node); return hooks?.main?.onReady?.(resolved, node); }); } } }; exports.handleCustomType = handleCustomType; /** * @type {Map}>} */ const registry = new Map(); /** * @typedef {Object} CustomOptions custom configuration * @prop {'pyodide' | 'micropython' | 'ruby-wasm-wasi' | 'wasmoon'} 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 */ let dontBotherCount = 0; /** * Allows custom types and components on the page to receive interpreters to execute any code * @param {string} type the unique `