@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
209 lines (208 loc) • 8.14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScriptEngine = void 0;
const Generic_1 = require("../../../utils/Generic");
const nonce_1 = require("../ctx/nonce");
const use_1 = require("../ctx/use");
const atomic_1 = require("./atomic");
const util_1 = require("./util");
class ScriptEngine {
/* Map storing the function bodies by id */
map_fn = new Map();
/* Map storing the data payloads with their id */
map_data = new Map();
/* Map storing modules */
map_modules = new Map();
/* Whether or not TriFrost atomic is enabled */
atomic_enabled = false;
/* Whether or not the Engine instance is in charge of root rendering */
root_renderer = false;
/* Mount path for root styles */
mount_path = null;
/* Known modules */
known_modules = {};
known_modules_rgx = null;
used_modules = new Set();
setAtomic(is_atomic) {
this.atomic_enabled = is_atomic === true;
}
setRoot(is_root) {
this.root_renderer = is_root === true;
}
setModules(modules) {
this.known_modules = modules;
this.known_modules_rgx = new RegExp(`\\$\\.(${Object.keys(modules).join('|')})\\.`, 'g');
}
/**
* Registers a script
*
* @param {string} fn - Function body for the script
* @param {string|null} data - Stringified data body or null
*/
register(fn, data) {
const minified_fn = (0, util_1.atomicMinify)(fn);
if (!minified_fn)
return {};
let fn_id = this.map_fn.get(minified_fn);
if (!fn_id) {
fn_id = (0, Generic_1.djb2Hash)(minified_fn);
this.map_fn.set(minified_fn, fn_id);
if (this.known_modules_rgx) {
const matches = minified_fn.matchAll(this.known_modules_rgx);
for (const match of matches) {
if (match[1] && !this.used_modules.has(match[1])) {
this.known_modules[match[1]]();
this.used_modules.add(match[1]);
}
}
}
}
let data_id = null;
if (data) {
data_id = this.map_data.get(data) || null;
if (!data_id) {
data_id = (0, Generic_1.djb2Hash)(data);
this.map_data.set(data, data_id);
}
}
return { fn_id, data_id };
}
/**
* Registers a module
*
* @param {string} fn - Function body for the module
* @param {string|null} data - Stringified data body or null
* @param {string} name - Name for the module
*/
registerModule(fn, data, name) {
const hash = (0, Generic_1.djb2Hash)(name);
if (this.map_modules.has(hash))
return { name: hash };
const minified_fn = (0, util_1.atomicMinify)(fn);
if (!minified_fn)
return {};
this.map_modules.set(hash, { fn: minified_fn, data, ogname: name });
this.used_modules.add(name);
return { name: hash };
}
/**
* Flushes the script registry into a string
*
* @param {ScriptEngineSeen} seen - Set of script and module hashes known to be on the client already
* @param {boolean} isFragment - (default=false) Whether or not we're flushing for a fragment
*/
flush(seen = { scripts: new Set(), modules: new Set() }, isFragment = false) {
let out = '';
/* Start modules */
if (this.map_modules.size) {
const MNS = [];
for (const [name, val] of [...this.map_modules]) {
if (!seen.modules.has(name)) {
let mod = '["' + name + '",' + val.fn + ',"' + val.ogname + '"';
if (val.data)
mod += ',' + val.data;
mod += ']';
MNS.push(mod);
seen.modules.add(name);
}
}
if (MNS.length)
out += `w.${atomic_1.GLOBAL_ARC_NAME}.sparkModule(${'[' + MNS.join(',') + ']'});`;
}
/* Start script */
if (this.map_fn.size) {
const FNS = [];
for (const [val, id] of [...this.map_fn]) {
if (!seen.scripts.has(id)) {
FNS.push('["' + id + '",' + val + ']');
seen.scripts.add(id);
}
else {
FNS.push('["' + id + '"]');
}
}
const DAT = '[' + [...this.map_data].map(([val, id]) => '["' + id + '",' + val + ']').join(',') + ']';
out += `w.${atomic_1.GLOBAL_ARC_NAME}.spark(${'[' + FNS.join(',') + ']'},${DAT},self?.parentNode);`;
}
if (!out.length)
return '';
/* Finalize iife */
if (!isFragment && this.mount_path && this.root_renderer) {
out = [
'(function(w){',
'const self=document.currentScript;',
'const run=()=>{',
out,
'setTimeout(()=>self?.remove?.(),0);',
'};',
`if(!w.${atomic_1.GLOBAL_ARC_NAME}){`,
`const wait=()=>{w.${atomic_1.GLOBAL_ARC_NAME}?run():setTimeout(wait,1)};`,
'wait();',
'}else{run()}',
'})(window);',
].join('');
}
else {
out = '(function(w){const self=document.currentScript;' + out + 'setTimeout(()=>self?.remove?.(),0);})(window);';
}
const n_nonce = (0, nonce_1.nonce)();
return n_nonce ? '<script nonce="' + n_nonce + '">' + out + '</script>' : '<script>' + out + '</script>';
}
inject(html, seen = { scripts: new Set(), modules: new Set() }) {
if (typeof html !== 'string')
return '';
const n_nonce = (0, nonce_1.nonce)();
const debug = (0, Generic_1.isDevMode)((0, use_1.getActiveCtx)()?.env ?? {});
const isFragment = !html.startsWith('<!DOCTYPE') && !html.startsWith('<html');
/* Mount script */
let scripts = '';
/* Add atomic/arc client runtime */
if (!isFragment) {
if (this.mount_path) {
scripts = n_nonce
? '<script nonce="' + n_nonce + '" src="' + this.mount_path + '" defer></script>'
: '<script src="' + this.mount_path + '" defer></script>';
}
else if (this.atomic_enabled) {
scripts = n_nonce
? '<script nonce="' + n_nonce + '">' + (0, atomic_1.ARC_GLOBAL)(debug) + atomic_1.ATOMIC_GLOBAL + '</script>'
: '<script>' + (0, atomic_1.ARC_GLOBAL)(debug) + atomic_1.ATOMIC_GLOBAL + '</script>';
}
else {
scripts = n_nonce
? '<script nonce="' + n_nonce + '">' + (0, atomic_1.ARC_GLOBAL)(debug) + atomic_1.ARC_GLOBAL_OBSERVER + '</script>'
: '<script>' + (0, atomic_1.ARC_GLOBAL)(debug) + atomic_1.ARC_GLOBAL_OBSERVER + '</script>';
}
seen.scripts.clear();
seen.modules.clear();
}
/* Add engine scripts */
scripts += this.flush(seen, isFragment);
if (isFragment)
return html + scripts;
const bodyIdx = html.indexOf('</body>');
if (bodyIdx >= 0)
return html.slice(0, bodyIdx) + scripts + html.slice(bodyIdx);
const htmlIdx = html.indexOf('</html>');
if (htmlIdx >= 0)
return html.slice(0, htmlIdx) + scripts + html.slice(htmlIdx);
return html + scripts;
}
reset() {
this.map_data = new Map();
this.map_fn = new Map();
this.map_modules = new Map();
this.known_modules = {};
this.known_modules_rgx = null;
this.used_modules = new Set();
}
/**
* Sets mount path for as-file renders of root scripts
*
* @param {string} path - Mount path for client root scripts
*/
setMountPath(path) {
this.mount_path = path;
}
}
exports.ScriptEngine = ScriptEngine;