|
'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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const types = new Map(); |
|
const waitList = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
}); |
|
} |
|
} |
|
}; |
|
exports.handleCustomType = handleCustomType; |
|
|
|
|
|
|
|
|
|
const registry = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let dontBotherCount = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
}; |
|
exports.define = define; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const whenDefined = (type) => { |
|
if (!waitList.has(type)) waitList.set(type, Promise.withResolvers()); |
|
return waitList.get(type).promise; |
|
}; |
|
exports.whenDefined = whenDefined; |
|
|
|
|