|
import '@ungap/with-resolvers'; |
|
import { $$ } from 'basic-devtools'; |
|
|
|
import { JSModules, assign, create, createOverload, createResolved, dedent, defineProperty, nodeInfo, registerJSModules } from './utils.js'; |
|
import { getDetails } from './script-handler.js'; |
|
import { registry as defaultRegistry, prefixes, configs } from './interpreters.js'; |
|
import { getRuntimeID } from './loader.js'; |
|
import { addAllListeners } from './listeners.js'; |
|
import { Hook, XWorker as XW } from './xworker.js'; |
|
import { polluteJS, js as jsHooks, code as codeHooks } from './hooks.js'; |
|
import workerURL from './worker/url.js'; |
|
|
|
export const CUSTOM_SELECTORS = []; |
|
|
|
export const customObserver = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const types = new Map(); |
|
const waitList = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
export 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; |
|
} |
|
} |
|
|
|
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, |
|
}); |
|
|
|
|
|
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; |
|
|
|
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); |
|
}); |
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
const registry = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let dontBotherCount = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const define = (type, options) => { |
|
|
|
let dontBother = type == null; |
|
|
|
if (dontBother) |
|
type = `_ps${dontBotherCount++}`; |
|
else if (defaultRegistry.has(type) || registry.has(type)) |
|
throw new Error(`<script type="${type}"> already registered`); |
|
|
|
if (!defaultRegistry.has(options?.interpreter)) |
|
throw new Error('Unspecified interpreter'); |
|
|
|
|
|
defaultRegistry.set(type, defaultRegistry.get(options.interpreter)); |
|
|
|
|
|
const selectors = [`script[type="${type}"]`]; |
|
|
|
|
|
whenDefined(type); |
|
|
|
if (dontBother) { |
|
|
|
const { hooks } = options; |
|
const onReady = hooks?.main?.onReady; |
|
options = { |
|
...options, |
|
hooks: { |
|
...hooks, |
|
main: { |
|
...hooks?.main, |
|
onReady(resolved, node) { |
|
CUSTOM_SELECTORS.splice(CUSTOM_SELECTORS.indexOf(type), 1); |
|
defaultRegistry.delete(type); |
|
registry.delete(type); |
|
waitList.delete(type); |
|
node.remove(); |
|
onReady?.(resolved); |
|
} |
|
} |
|
}, |
|
}; |
|
document.head.append( |
|
assign(document.createElement('script'), { type }) |
|
); |
|
} |
|
else { |
|
selectors.push(`${type}-script`); |
|
prefixes.push(`${type}-`); |
|
} |
|
|
|
for (const selector of selectors) types.set(selector, type); |
|
CUSTOM_SELECTORS.push(...selectors); |
|
|
|
|
|
registry.set(type, { |
|
options: assign({ env: type }, options), |
|
known: new WeakSet(), |
|
queue: Promise.resolve(), |
|
}); |
|
|
|
if (!dontBother) addAllListeners(document); |
|
$$(selectors.join(',')).forEach(handleCustomType); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const whenDefined = (type) => { |
|
if (!waitList.has(type)) waitList.set(type, Promise.withResolvers()); |
|
return waitList.get(type).promise; |
|
}; |
|
|
|
|