@webqit/oohtml
Version:
A suite of new DOM features that brings language support for modern UI development paradigms: a component-based architecture, data binding, and reactivity.
137 lines (129 loc) • 6.88 kB
JavaScript
import { isElement } from '@webqit/realdom';
import { resolveParams } from '@webqit/use-live/params';
import { _wq, _init, _toHash, _fromHash } from '../util.js';
export default function init({ advanced = {}, ...$config }) {
const { config, window } = _init.call(this, 'scoped-js', $config, {
script: { retention: 'retain', mimeTypes: 'module|text/javascript|application/javascript', timing: 'auto' },
api: { scripts: 'scripts' },
advanced: resolveParams(advanced),
});
const customTypes = Array.isArray(config.script.mimeTypes) ? config.script.mimeTypes : config.script.mimeTypes.split('|').filter(t => t);
config.scriptSelector = customTypes.map(t => `script[type="${window.CSS.escape(t)}"]:not([oohtmlignore])`).concat(`script:not([type])`).join(',');
window.webqit.oohtml.Script = {
compileCache: [new Map, new Map,],
execute: execute.bind(window, config),
};
exposeAPIs.call(window, config);
realtime.call(window, config);
}
function exposeAPIs(config) {
const window = this, { webqit: { nextKeyword, matchPrologDirective } } = window;
const scriptsMap = new Map;
if (config.api.scripts in window.Element.prototype) { throw new Error(`The "Element" class already has a "${config.api.scripts}" property!`); }
[window.ShadowRoot.prototype, window.Element.prototype].forEach(proto => {
Object.defineProperty(proto, config.api.scripts, {
get: function () {
if (!scriptsMap.has(this)) { scriptsMap.set(this, []); }
return scriptsMap.get(this);
},
});
});
Object.defineProperties(window.HTMLScriptElement.prototype, {
scoped: {
configurable: true,
get() { return this.hasAttribute('scoped'); },
set(value) { this.toggleAttribute('scoped', value); },
},
live: {
configurable: true,
get() {
if (this.liveProgramHandle) return true;
const scriptContents = nextKeyword(this.oohtml__textContent || this.textContent || '', 0, 0);
return matchPrologDirective(scriptContents, true);
},
},
});
}
// Script runner
async function execute(config, execHash) {
const window = this, { realdom } = window.webqit;
const exec = _fromHash(execHash);
if (!exec) throw new Error(`Argument must be a valid exec hash.`);
const { script, compiledScript, thisContext } = exec;
// Honour retention flag
if (config.script.retention === 'dispose') {
script.remove();
} else if (config.script.retention === 'hidden') {
script.textContent = `"source hidden"`;
} else {
setTimeout(async () => {
script.textContent = await compiledScript.toString();
}, 0); //Anti-eval hack
}
// Execute and save state
const varScope = script.scoped ? thisContext : script.getRootNode();
if (!_wq(varScope).has('scriptEnv')) {
_wq(varScope).set('scriptEnv', Object.create(null));
}
const liveProgramHandle = await (await compiledScript.bind(thisContext, _wq(varScope).get('scriptEnv'))).execute();
if (script.live) { Object.defineProperty(script, 'liveProgramHandle', { value: liveProgramHandle }); }
realdom.realtime(window.document).observe(script, () => {
if (script.live) { liveProgramHandle.abort(); }
if (isElement(thisContext)) { thisContext[config.api.scripts]?.splice(thisContext[config.api.scripts].indexOf(script, 1)); }
}, { id: 'scoped-js:script-exits', subtree: 'cross-roots', timing: 'sync', generation: 'exits' });
}
function realtime(config) {
const inBrowser = Object.getOwnPropertyDescriptor(globalThis, 'window')?.get?.toString().includes('[native code]') ?? false;
const window = this, { webqit: { oohtml, realdom } } = window;
if (!window.HTMLScriptElement.supports) { window.HTMLScriptElement.supports = type => ['text/javascript', 'application/javascript'].includes(type); }
const handled = new WeakSet;
realdom.realtime(window.document).query(config.scriptSelector, record => {
record.entrants.forEach(script => {
if (handled.has(script) || script.hasAttribute('oohtmlno') || (!inBrowser && !script.hasAttribute('ssr'))) return;
// Do compilation
const compiledScript = compileScript.call(window, config, script);
if (!compiledScript) return;
handled.add(script);
// Run now!!!
const thisContext = script.scoped ? script.parentNode || record.target : (script.type === 'module' ? undefined : window);
if (script.scoped) { thisContext[config.api.scripts].push(script); }
const execHash = _toHash({ script, compiledScript, thisContext });
const manualHandling = record.type === 'query' || (script.type && !window.HTMLScriptElement.supports(script.type)) || script.getAttribute('data-handling') === 'manual';
if (manualHandling || config.script.timing === 'manual') { oohtml.Script.execute(execHash); } else {
script.textContent = `webqit.oohtml.Script.execute( '${execHash}' );`;
}
});
}, { id: 'scoped-js:script-entries', live: true, subtree: 'cross-roots', timing: 'intercept', generation: 'entrants', eventDetails: true });
// ---
}
function compileScript(config, script) {
const window = this, { webqit: { oohtml, LiveScript, AsyncLiveScript, LiveModule } } = window;
let textContent = script.textContent.trim();
if (textContent.startsWith('/*@oohtml*/if(false){') && textContent.endsWith('}/*@oohtml*/')) {
textContent = textContent.slice(21, -12);
Object.defineProperty(script, 'oohtml__textContent', { value: textContent });
}
if (!textContent.trim().length) return;
const sourceHash = _toHash(textContent);
const compileCache = oohtml.Script.compileCache[script.live ? 0 : 1];
let compiledScript;
if (!(compiledScript = compileCache.get(sourceHash))) {
const { parserParams, compilerParams, runtimeParams } = config.advanced;
compiledScript = new (script.type === 'module' ? LiveModule : (LiveScript || AsyncLiveScript))(textContent, {
liveMode: script.live,
exportNamespace: `#${script.id}`,
fileName: `${window.document.url?.split('#')?.[0] || ''}#${script.id}`,
parserParams,
compilerParams,
runtimeParams,
});
compileCache.set(sourceHash, compiledScript);
}
return compiledScript;
}
export function idleCompiler(node) {
const window = this, { webqit: { oohtml: { configs: { SCOPED_JS: config } } } } = window;
[...(node?.querySelectorAll(config.scriptSelector) || [])].forEach(script => {
compileScript.call(window, config, script);
});
}